aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs2
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs201
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs15
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs1
-rw-r--r--MediaBrowser.Api/Attachments/AttachmentService.cs63
-rw-r--r--MediaBrowser.Api/Playback/MediaInfoService.cs10
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs1
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs14
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs17
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs15
-rw-r--r--MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs20
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs281
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs37
-rw-r--r--MediaBrowser.MediaEncoding/packages.config3
-rw-r--r--MediaBrowser.Model/Dto/MediaSourceInfo.cs2
-rw-r--r--MediaBrowser.Model/Entities/MediaAttachment.cs50
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs10
17 files changed, 735 insertions, 7 deletions
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 9b853c6d1..a179c1b15 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -875,6 +875,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(typeof(IResourceFileManager), typeof(ResourceFileManager));
serviceCollection.AddSingleton<EncodingHelper>();
+ serviceCollection.AddSingleton(typeof(IAttachmentExtractor), typeof(MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor));
+
_displayPreferencesRepository.Initialize();
var userDataRepo = new SqliteUserDataRepository(LoggerFactory.CreateLogger<SqliteUserDataRepository>(), ApplicationPaths);
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index ef1fe07c1..c514846e5 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -49,6 +49,21 @@ namespace Emby.Server.Implementations.Data
private readonly TypeMapper _typeMapper;
private readonly JsonSerializerOptions _jsonOptions;
+ static SqliteItemRepository()
+ {
+ var queryPrefixText = new StringBuilder();
+ queryPrefixText.Append("insert into mediaattachments (");
+ foreach (var column in _mediaAttachmentSaveColumns)
+ {
+ queryPrefixText.Append(column)
+ .Append(',');
+ }
+
+ queryPrefixText.Length -= 1;
+ queryPrefixText.Append(") values ");
+ _mediaAttachmentInsertPrefix = queryPrefixText.ToString();
+ }
+
/// <summary>
/// Initializes a new instance of the <see cref="SqliteItemRepository"/> class.
/// </summary>
@@ -92,6 +107,8 @@ namespace Emby.Server.Implementations.Data
{
const string CreateMediaStreamsTableCommand
= "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))";
+ const string CreateMediaAttachmentsTableCommand
+ = "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))";
string[] queries =
{
@@ -114,6 +131,7 @@ namespace Emby.Server.Implementations.Data
"create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))",
CreateMediaStreamsTableCommand,
+ CreateMediaAttachmentsTableCommand,
"pragma shrink_memory"
};
@@ -421,6 +439,19 @@ namespace Emby.Server.Implementations.Data
"ColorTransfer"
};
+ private static readonly string[] _mediaAttachmentSaveColumns =
+ {
+ "ItemId",
+ "AttachmentIndex",
+ "Codec",
+ "CodecTag",
+ "Comment",
+ "Filename",
+ "MIMEType"
+ };
+
+ private static readonly string _mediaAttachmentInsertPrefix;
+
private static string GetSaveItemCommandText()
{
var saveColumns = new []
@@ -6136,5 +6167,175 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
return item;
}
+
+ public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
+ {
+ CheckDisposed();
+
+ if (query == null)
+ {
+ throw new ArgumentNullException(nameof(query));
+ }
+
+ var cmdText = "select "
+ + string.Join(",", _mediaAttachmentSaveColumns)
+ + " from mediaattachments where"
+ + " ItemId=@ItemId";
+
+ if (query.Index.HasValue)
+ {
+ cmdText += " AND AttachmentIndex=@AttachmentIndex";
+ }
+
+ cmdText += " order by AttachmentIndex ASC";
+
+ var list = new List<MediaAttachment>();
+ using (var connection = GetConnection(true))
+ using (var statement = PrepareStatement(connection, cmdText))
+ {
+ statement.TryBind("@ItemId", query.ItemId.ToByteArray());
+
+ if (query.Index.HasValue)
+ {
+ statement.TryBind("@AttachmentIndex", query.Index.Value);
+ }
+
+ foreach (var row in statement.ExecuteQuery())
+ {
+ list.Add(GetMediaAttachment(row));
+ }
+ }
+
+ return list;
+ }
+
+ public void SaveMediaAttachments(
+ Guid id,
+ IReadOnlyList<MediaAttachment> attachments,
+ CancellationToken cancellationToken)
+ {
+ CheckDisposed();
+ if (id == Guid.Empty)
+ {
+ throw new ArgumentException(nameof(id));
+ }
+
+ if (attachments == null)
+ {
+ throw new ArgumentNullException(nameof(attachments));
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ using (var connection = GetConnection())
+ {
+ connection.RunInTransaction(db =>
+ {
+ var itemIdBlob = id.ToByteArray();
+
+ db.Execute("delete from mediaattachments where ItemId=@ItemId", itemIdBlob);
+
+ InsertMediaAttachments(itemIdBlob, attachments, db, cancellationToken);
+
+ }, TransactionMode);
+ }
+ }
+
+ private void InsertMediaAttachments(
+ byte[] idBlob,
+ IReadOnlyList<MediaAttachment> attachments,
+ IDatabaseConnection db,
+ CancellationToken cancellationToken)
+ {
+ const int InsertAtOnce = 10;
+
+ for (var startIndex = 0; startIndex < attachments.Count; startIndex += InsertAtOnce)
+ {
+ var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
+
+ var endIndex = Math.Min(attachments.Count, startIndex + InsertAtOnce);
+
+ for (var i = startIndex; i < endIndex; i++)
+ {
+ var index = i.ToString(CultureInfo.InvariantCulture);
+ insertText.Append("(@ItemId, ");
+
+ foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
+ {
+ insertText.Append("@" + column + index + ",");
+ }
+
+ insertText.Length -= 1;
+
+ insertText.Append("),");
+ }
+
+ insertText.Length--;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ using (var statement = PrepareStatement(db, insertText.ToString()))
+ {
+ statement.TryBind("@ItemId", idBlob);
+
+ for (var i = startIndex; i < endIndex; i++)
+ {
+ var index = i.ToString(CultureInfo.InvariantCulture);
+
+ var attachment = attachments[i];
+
+ statement.TryBind("@AttachmentIndex" + index, attachment.Index);
+ statement.TryBind("@Codec" + index, attachment.Codec);
+ statement.TryBind("@CodecTag" + index, attachment.CodecTag);
+ statement.TryBind("@Comment" + index, attachment.Comment);
+ statement.TryBind("@FileName" + index, attachment.FileName);
+ statement.TryBind("@MimeType" + index, attachment.MimeType);
+ }
+
+ statement.Reset();
+ statement.MoveNext();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the attachment.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <returns>MediaAttachment</returns>
+ private MediaAttachment GetMediaAttachment(IReadOnlyList<IResultSetValue> reader)
+ {
+ var item = new MediaAttachment
+ {
+ Index = reader[1].ToInt()
+ };
+
+ if (reader[2].SQLiteType != SQLiteType.Null)
+ {
+ item.Codec = reader[2].ToString();
+ }
+
+ if (reader[2].SQLiteType != SQLiteType.Null)
+ {
+ item.CodecTag = reader[3].ToString();
+ }
+
+ if (reader[4].SQLiteType != SQLiteType.Null)
+ {
+ item.Comment = reader[4].ToString();
+ }
+
+ if (reader[6].SQLiteType != SQLiteType.Null)
+ {
+ item.FileName = reader[5].ToString();
+ }
+
+ if (reader[6].SQLiteType != SQLiteType.Null)
+ {
+ item.MimeType = reader[6].ToString();
+ }
+
+ return item;
+ }
}
}
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 22193c997..ba1564d1f 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -130,6 +130,21 @@ namespace Emby.Server.Implementations.Library
return streams;
}
+ /// <inheritdoc />
+ public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
+ {
+ return _itemRepo.GetMediaAttachments(query);
+ }
+
+ /// <inheritdoc />
+ public List<MediaAttachment> GetMediaAttachments(Guid itemId)
+ {
+ return GetMediaAttachments(new MediaAttachmentQuery
+ {
+ ItemId = itemId
+ });
+ }
+
public async Task<List<MediaSourceInfo>> GetPlayackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
{
var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index 1014c8c56..afc9b8f3d 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -96,7 +96,6 @@ namespace Jellyfin.Api.Controllers
public StartupUserDto GetFirstUser()
{
var user = _userManager.Users.First();
-
return new StartupUserDto
{
Name = user.Name,
diff --git a/MediaBrowser.Api/Attachments/AttachmentService.cs b/MediaBrowser.Api/Attachments/AttachmentService.cs
new file mode 100644
index 000000000..1632ca1b0
--- /dev/null
+++ b/MediaBrowser.Api/Attachments/AttachmentService.cs
@@ -0,0 +1,63 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Services;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Api.Attachments
+{
+ [Route("/Videos/{Id}/{MediaSourceId}/Attachments/{Index}", "GET", Summary = "Gets specified attachment.")]
+ public class GetAttachment
+ {
+ [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+ public Guid Id { get; set; }
+
+ [ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+ public string MediaSourceId { get; set; }
+
+ [ApiMember(Name = "Index", Description = "The attachment stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
+ public int Index { get; set; }
+ }
+
+ public class AttachmentService : BaseApiService
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly IAttachmentExtractor _attachmentExtractor;
+
+ public AttachmentService(
+ ILogger<AttachmentService> logger,
+ IServerConfigurationManager serverConfigurationManager,
+ IHttpResultFactory httpResultFactory,
+ ILibraryManager libraryManager,
+ IAttachmentExtractor attachmentExtractor)
+ : base(logger, serverConfigurationManager, httpResultFactory)
+ {
+ _libraryManager = libraryManager;
+ _attachmentExtractor = attachmentExtractor;
+ }
+
+ public async Task<object> Get(GetAttachment request)
+ {
+ var (attachment, attachmentStream) = await GetAttachment(request).ConfigureAwait(false);
+ var mime = string.IsNullOrWhiteSpace(attachment.MimeType) ? "application/octet-stream" : attachment.MimeType;
+
+ return ResultFactory.GetResult(Request, attachmentStream, mime);
+ }
+
+ private Task<(MediaAttachment, Stream)> GetAttachment(GetAttachment request)
+ {
+ var item = _libraryManager.GetItemById(request.Id);
+
+ return _attachmentExtractor.GetAttachment(item,
+ request.MediaSourceId,
+ request.Index,
+ CancellationToken.None);
+ }
+ }
+}
diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs
index c3032416b..7375e1509 100644
--- a/MediaBrowser.Api/Playback/MediaInfoService.cs
+++ b/MediaBrowser.Api/Playback/MediaInfoService.cs
@@ -522,6 +522,16 @@ namespace MediaBrowser.Api.Playback
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
}
}
+
+ foreach (var attachment in mediaSource.MediaAttachments)
+ {
+ attachment.DeliveryUrl = string.Format(
+ CultureInfo.InvariantCulture,
+ "/Videos/{0}/{1}/Attachments/{2}",
+ item.Id,
+ mediaSource.Id,
+ attachment.Index);
+ }
}
private long? GetMaxBitrate(long? clientMaxBitrate, User user)
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 1fd706857..cba2c9dda 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -1098,6 +1098,7 @@ namespace MediaBrowser.Controller.Entities
Id = item.Id.ToString("N", CultureInfo.InvariantCulture),
Protocol = protocol ?? MediaProtocol.File,
MediaStreams = MediaSourceManager.GetMediaStreams(item.Id),
+ MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id),
Name = GetMediaSourceName(item),
Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path,
RunTimeTicks = item.RunTimeTicks,
diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
index fbae4edb0..09e6fda88 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
@@ -39,6 +39,20 @@ namespace MediaBrowser.Controller.Library
List<MediaStream> GetMediaStreams(MediaStreamQuery query);
/// <summary>
+ /// Gets the media attachments.
+ /// </summary>
+ /// <param name="itemId">The item identifier.</param>
+ /// <returns>IEnumerable&lt;MediaAttachment&gt;.</returns>
+ List<MediaAttachment> GetMediaAttachments(Guid itemId);
+
+ /// <summary>
+ /// Gets the media attachments.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>IEnumerable&lt;MediaAttachment&gt;.</returns>
+ List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query);
+
+ /// <summary>
/// Gets the playack media sources.
/// </summary>
Task<List<MediaSourceInfo>> GetPlayackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken);
diff --git a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs
new file mode 100644
index 000000000..7c7e84de6
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs
@@ -0,0 +1,17 @@
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.MediaEncoding
+{
+ public interface IAttachmentExtractor
+ {
+ Task<(MediaAttachment attachment, Stream stream)> GetAttachment(
+ BaseItem item,
+ string mediaSourceId,
+ int attachmentStreamIndex,
+ CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index 47e0f3453..5a5b7f58f 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -79,6 +79,21 @@ namespace MediaBrowser.Controller.Persistence
void SaveMediaStreams(Guid id, List<MediaStream> streams, CancellationToken cancellationToken);
/// <summary>
+ /// Gets the media attachments.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>IEnumerable{MediaAttachment}.</returns>
+ List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query);
+
+ /// <summary>
+ /// Saves the media attachments.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <param name="attachments">The attachments.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void SaveMediaAttachments(Guid id, IReadOnlyList<MediaAttachment> attachments, CancellationToken cancellationToken);
+
+ /// <summary>
/// Gets the item ids.
/// </summary>
/// <param name="query">The query.</param>
diff --git a/MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs b/MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs
new file mode 100644
index 000000000..91ab34aab
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs
@@ -0,0 +1,20 @@
+using System;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Persistence
+{
+ public class MediaAttachmentQuery
+ {
+ /// <summary>
+ /// Gets or sets the index.
+ /// </summary>
+ /// <value>The index.</value>
+ public int? Index { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item identifier.
+ /// </summary>
+ /// <value>The item identifier.</value>
+ public Guid ItemId { get; set; }
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
new file mode 100644
index 000000000..c371e8b94
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -0,0 +1,281 @@
+using System;
+using System.Diagnostics;
+using System.Collections.Concurrent;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.MediaEncoding.Attachments
+{
+ public class AttachmentExtractor : IAttachmentExtractor, IDisposable
+ {
+ private readonly ILogger _logger;
+ private readonly IApplicationPaths _appPaths;
+ private readonly IFileSystem _fileSystem;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IMediaSourceManager _mediaSourceManager;
+
+ private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
+ new ConcurrentDictionary<string, SemaphoreSlim>();
+
+ private bool _disposed = false;
+
+ public AttachmentExtractor(
+ ILogger<AttachmentExtractor> logger,
+ IApplicationPaths appPaths,
+ IFileSystem fileSystem,
+ IMediaEncoder mediaEncoder,
+ IMediaSourceManager mediaSourceManager)
+ {
+ _logger = logger;
+ _appPaths = appPaths;
+ _fileSystem = fileSystem;
+ _mediaEncoder = mediaEncoder;
+ _mediaSourceManager = mediaSourceManager;
+ }
+
+ /// <inheritdoc />
+ public async Task<(MediaAttachment attachment, Stream stream)> GetAttachment(BaseItem item, string mediaSourceId, int attachmentStreamIndex, CancellationToken cancellationToken)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException(nameof(item));
+ }
+
+ 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}");
+ }
+
+ var attachmentStream = await GetAttachmentStream(mediaSource, mediaAttachment, cancellationToken)
+ .ConfigureAwait(false);
+
+ return (mediaAttachment, attachmentStream);
+ }
+
+ private async Task<Stream> GetAttachmentStream(
+ MediaSourceInfo mediaSource,
+ MediaAttachment mediaAttachment,
+ CancellationToken cancellationToken)
+ {
+ var attachmentPath = await GetReadableFile(mediaSource.Path, mediaSource.Path, mediaSource.Protocol, mediaAttachment, cancellationToken).ConfigureAwait(false);
+ return File.OpenRead(attachmentPath);
+ }
+
+ private async Task<string> GetReadableFile(
+ string mediaPath,
+ string inputFile,
+ MediaProtocol protocol,
+ MediaAttachment mediaAttachment,
+ CancellationToken cancellationToken)
+ {
+ var outputPath = GetAttachmentCachePath(mediaPath, protocol, mediaAttachment.Index);
+ await ExtractAttachment(inputFile, protocol, mediaAttachment.Index, outputPath, cancellationToken)
+ .ConfigureAwait(false);
+
+ return outputPath;
+ }
+
+ private async Task ExtractAttachment(
+ string inputFile,
+ MediaProtocol protocol,
+ int attachmentStreamIndex,
+ string outputPath,
+ CancellationToken cancellationToken)
+ {
+ var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1));
+
+ await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ if (!File.Exists(outputPath))
+ {
+ await ExtractAttachmentInternal(
+ _mediaEncoder.GetInputArgument(new[] { inputFile }, 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
+ {
+ Arguments = processArgs,
+ FileName = _mediaEncoder.EncoderPath,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ };
+ var process = new Process
+ {
+ StartInfo = startInfo
+ };
+
+ _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
+
+ process.Start();
+
+ 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();
+
+ 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)
+ {
+ failed = true;
+
+ _logger.LogWarning("Deleting extracted attachment {Path} due to failure: {ExitCode}", outputPath, exitCode);
+ try
+ {
+ if (File.Exists(outputPath))
+ {
+ _fileSystem.DeleteFile(outputPath);
+ }
+ }
+ 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 InvalidOperationException(msg);
+ }
+ else
+ {
+ _logger.LogInformation("ffmpeg attachment extraction completed for {Path} to {Path}", inputPath, outputPath);
+ }
+ }
+
+ private string GetAttachmentCachePath(string mediaPath, MediaProtocol protocol, int attachmentStreamIndex)
+ {
+ string filename;
+ if (protocol == MediaProtocol.File)
+ {
+ var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
+ filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D");
+ }
+ else
+ {
+ filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D");
+ }
+
+ var prefix = filename.Substring(0, 1);
+ return Path.Combine(_appPaths.DataPath, "attachments", prefix, filename);
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+
+ }
+
+ _disposed = true;
+ }
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index ff3596a85..a46aa38d8 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -49,6 +49,10 @@ namespace MediaBrowser.MediaEncoding.Probing
.Where(i => i.Type != MediaStreamType.Subtitle || !string.IsNullOrWhiteSpace(i.Codec))
.ToList();
+ info.MediaAttachments = internalStreams.Select(s => GetMediaAttachment(s))
+ .Where(i => i != null)
+ .ToList();
+
if (data.format != null)
{
info.Container = NormalizeFormat(data.format.format_name);
@@ -514,6 +518,39 @@ namespace MediaBrowser.MediaEncoding.Probing
}
/// <summary>
+ /// Converts ffprobe stream info to our MediaAttachment class
+ /// </summary>
+ /// <param name="streamInfo">The stream info.</param>
+ /// <returns>MediaAttachments.</returns>
+ private MediaAttachment GetMediaAttachment(MediaStreamInfo streamInfo)
+ {
+ if (!string.Equals(streamInfo.codec_type, "attachment", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ var attachment = new MediaAttachment
+ {
+ Codec = streamInfo.codec_name,
+ Index = streamInfo.index
+ };
+
+ if (!string.IsNullOrWhiteSpace(streamInfo.codec_tag_string))
+ {
+ attachment.CodecTag = streamInfo.codec_tag_string;
+ }
+
+ if (streamInfo.tags != null)
+ {
+ attachment.FileName = GetDictionaryValue(streamInfo.tags, "filename");
+ attachment.MimeType = GetDictionaryValue(streamInfo.tags, "mimetype");
+ attachment.Comment = GetDictionaryValue(streamInfo.tags, "comment");
+ }
+
+ return attachment;
+ }
+
+ /// <summary>
/// Converts ffprobe stream info to our MediaStream class
/// </summary>
/// <param name="isAudio">if set to <c>true</c> [is info].</param>
diff --git a/MediaBrowser.MediaEncoding/packages.config b/MediaBrowser.MediaEncoding/packages.config
deleted file mode 100644
index bbeaf5f00..000000000
--- a/MediaBrowser.MediaEncoding/packages.config
+++ /dev/null
@@ -1,3 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<packages>
-</packages> \ No newline at end of file
diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
index 5bdc4809a..5cb056566 100644
--- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs
+++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
@@ -57,6 +57,8 @@ namespace MediaBrowser.Model.Dto
public List<MediaStream> MediaStreams { get; set; }
+ public IReadOnlyList<MediaAttachment> MediaAttachments { get; set; }
+
public string[] Formats { get; set; }
public int? Bitrate { get; set; }
diff --git a/MediaBrowser.Model/Entities/MediaAttachment.cs b/MediaBrowser.Model/Entities/MediaAttachment.cs
new file mode 100644
index 000000000..8f8c3efd2
--- /dev/null
+++ b/MediaBrowser.Model/Entities/MediaAttachment.cs
@@ -0,0 +1,50 @@
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Class MediaAttachment
+ /// </summary>
+ public class MediaAttachment
+ {
+ /// <summary>
+ /// Gets or sets the codec.
+ /// </summary>
+ /// <value>The codec.</value>
+ public string Codec { get; set; }
+
+ /// <summary>
+ /// Gets or sets the codec tag.
+ /// </summary>
+ /// <value>The codec tag.</value>
+ public string CodecTag { get; set; }
+
+ /// <summary>
+ /// Gets or sets the comment.
+ /// </summary>
+ /// <value>The comment.</value>
+ public string Comment { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index.
+ /// </summary>
+ /// <value>The index.</value>
+ public int Index { get; set; }
+
+ /// <summary>
+ /// Gets or sets the filename.
+ /// </summary>
+ /// <value>The filename.</value>
+ public string FileName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the MIME type.
+ /// </summary>
+ /// <value>The MIME type.</value>
+ public string MimeType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the delivery URL.
+ /// </summary>
+ /// <value>The delivery URL.</value>
+ public string DeliveryUrl { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index d2abd2a63..2b178d4d4 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -158,11 +158,13 @@ namespace MediaBrowser.Providers.MediaInfo
MetadataRefreshOptions options)
{
List<MediaStream> mediaStreams;
+ IReadOnlyList<MediaAttachment> mediaAttachments;
List<ChapterInfo> chapters;
if (mediaInfo != null)
{
mediaStreams = mediaInfo.MediaStreams;
+ mediaAttachments = mediaInfo.MediaAttachments;
video.TotalBitrate = mediaInfo.Bitrate;
//video.FormatName = (mediaInfo.Container ?? string.Empty)
@@ -198,6 +200,7 @@ namespace MediaBrowser.Providers.MediaInfo
else
{
mediaStreams = new List<MediaStream>();
+ mediaAttachments = Array.Empty<MediaAttachment>();
chapters = new List<ChapterInfo>();
}
@@ -210,19 +213,20 @@ namespace MediaBrowser.Providers.MediaInfo
FetchEmbeddedInfo(video, mediaInfo, options, libraryOptions);
FetchPeople(video, mediaInfo, options);
video.Timestamp = mediaInfo.Timestamp;
- video.Video3DFormat = video.Video3DFormat ?? mediaInfo.Video3DFormat;
+ video.Video3DFormat ??= mediaInfo.Video3DFormat;
}
var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
- video.Height = videoStream == null ? 0 : videoStream.Height ?? 0;
- video.Width = videoStream == null ? 0 : videoStream.Width ?? 0;
+ video.Height = videoStream?.Height ?? 0;
+ video.Width = videoStream?.Width ?? 0;
video.DefaultVideoStreamIndex = videoStream == null ? (int?)null : videoStream.Index;
video.HasSubtitles = mediaStreams.Any(i => i.Type == MediaStreamType.Subtitle);
_itemRepo.SaveMediaStreams(video.Id, mediaStreams, cancellationToken);
+ _itemRepo.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken);
if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh ||
options.MetadataRefreshMode == MetadataRefreshMode.Default)