aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs6
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs179
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs26
-rw-r--r--MediaBrowser.Api/Attachments/AttachmentService.cs70
-rw-r--r--MediaBrowser.Api/Playback/MediaInfoService.cs10
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs1
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs19
-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.cs277
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs38
-rw-r--r--MediaBrowser.Model/Dto/MediaSourceInfo.cs2
-rw-r--r--MediaBrowser.Model/Entities/MediaAttachment.cs50
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs4
15 files changed, 734 insertions, 0 deletions
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index f7fe2bd63..ac37cfe07 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -282,6 +282,8 @@ namespace Emby.Server.Implementations
private ISubtitleEncoder SubtitleEncoder { get; set; }
+ private IAttachmentExtractor AttachmentExtractor { get; set; }
+
private ISessionManager SessionManager { get; set; }
private ILiveTvManager LiveTvManager { get; set; }
@@ -904,6 +906,10 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(typeof(IResourceFileManager), typeof(ResourceFileManager));
+ AttachmentExtractor = new MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor(LibraryManager, LoggerFactory, ApplicationPaths, FileSystemManager, MediaEncoder, MediaSourceManager, ProcessFactory);
+
+ serviceCollection.AddSingleton(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 33402f0e3..08f53f0f5 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -94,6 +94,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 =
{
@@ -116,6 +118,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"
};
@@ -423,6 +426,17 @@ namespace Emby.Server.Implementations.Data
"ColorTransfer"
};
+ private static readonly string[] _mediaAttachmentSaveColumns =
+ {
+ "ItemId",
+ "AttachmentIndex",
+ "Codec",
+ "CodecTag",
+ "Comment",
+ "Filename",
+ "MIMEType"
+ };
+
private static string GetSaveItemCommandText()
{
var saveColumns = new []
@@ -6133,5 +6147,170 @@ 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";
+
+ using (var connection = GetConnection(true))
+ {
+ var list = new List<MediaAttachment>();
+
+ using (var statement = PrepareStatement(connection, cmdText))
+ {
+ statement.TryBind("@ItemId", query.ItemId.ToGuidBlob());
+
+ 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, List<MediaAttachment> attachments, CancellationToken cancellationToken)
+ {
+ CheckDisposed();
+ if (id == Guid.Empty)
+ {
+ throw new ArgumentNullException(nameof(id));
+ }
+
+ if (attachments == null)
+ {
+ throw new ArgumentNullException(nameof(attachments));
+ }
+
+ using (var connection = GetConnection())
+ {
+ connection.RunInTransaction(db =>
+ {
+ var itemIdBlob = id.ToGuidBlob();
+
+ db.Execute("delete from mediaattachments where ItemId=@ItemId", itemIdBlob);
+
+ InsertMediaAttachments(itemIdBlob, attachments, db);
+
+ }, TransactionMode);
+ }
+ }
+
+ private void InsertMediaAttachments(byte[] idBlob, List<MediaAttachment> attachments, IDatabaseConnection db)
+ {
+ var startIndex = 0;
+ var limit = 10;
+
+ while (startIndex < attachments.Count)
+ {
+ var insertText = new StringBuilder(string.Format("insert into mediaattachments ({0}) values ", string.Join(",", _mediaAttachmentSaveColumns)));
+
+ var endIndex = Math.Min(attachments.Count, startIndex + limit);
+
+ for (var i = startIndex; i < endIndex; i++)
+ {
+ if (i != startIndex)
+ {
+ insertText.Append(",");
+ }
+
+ 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(")");
+ }
+
+ 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();
+ }
+ startIndex += limit;
+ }
+ }
+
+ /// <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 7a26e0c37..2e24b847b 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -128,6 +128,32 @@ namespace Emby.Server.Implementations.Library
return streams;
}
+ public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
+ {
+ var list = _itemRepo.GetMediaAttachments(query);
+ return list;
+ }
+
+ public List<MediaAttachment> GetMediaAttachments(string mediaSourceId)
+ {
+ var list = GetMediaAttachments(new MediaAttachmentQuery
+ {
+ ItemId = new Guid(mediaSourceId)
+ });
+
+ return list;
+ }
+
+ public List<MediaAttachment> GetMediaAttachments(Guid itemId)
+ {
+ var list = GetMediaAttachments(new MediaAttachmentQuery
+ {
+ ItemId = itemId
+ });
+
+ return list;
+ }
+
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/MediaBrowser.Api/Attachments/AttachmentService.cs b/MediaBrowser.Api/Attachments/AttachmentService.cs
new file mode 100644
index 000000000..d8771f8c0
--- /dev/null
+++ b/MediaBrowser.Api/Attachments/AttachmentService.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Services;
+using Microsoft.Extensions.Logging;
+using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
+
+namespace MediaBrowser.Api.Attachments
+{
+ [Route("/Videos/{Id}/{MediaSourceId}/Attachments/{Index}/{Filename}", "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; }
+
+ [ApiMember(Name = "Filename", Description = "The attachment filename", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
+ public string Filename { get; set; }
+ }
+
+ public class AttachmentService : BaseApiService
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly IAttachmentExtractor _attachmentExtractor;
+
+ public AttachmentService(ILibraryManager libraryManager, IAttachmentExtractor attachmentExtractor)
+ {
+ _libraryManager = libraryManager;
+ _attachmentExtractor = attachmentExtractor;
+ }
+
+ public async Task<object> Get(GetAttachment request)
+ {
+ var item = (Video)_libraryManager.GetItemById(request.Id);
+ 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 da8f99a3d..41b324975 100644
--- a/MediaBrowser.Api/Playback/MediaInfoService.cs
+++ b/MediaBrowser.Api/Playback/MediaInfoService.cs
@@ -524,6 +524,16 @@ namespace MediaBrowser.Api.Playback
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
}
}
+
+ foreach (var attachment in mediaSource.MediaAttachments)
+ {
+ var filename = string.IsNullOrWhiteSpace(attachment.Filename) ? "Attachment" : attachment.Filename;
+ attachment.DeliveryUrl = string.Format("/Videos/{0}/{1}/Attachments/{2}/{3}",
+ item.Id,
+ mediaSource.Id,
+ attachment.Index,
+ filename);
+ }
}
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..961028b04 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
@@ -39,6 +39,25 @@ namespace MediaBrowser.Controller.Library
List<MediaStream> GetMediaStreams(MediaStreamQuery query);
/// <summary>
+ /// Gets the media attachments.
+ /// </summary>
+ /// <param name="">The item identifier.</param>
+ /// <returns>IEnumerable&lt;MediaAttachment&gt;.</returns>
+ List<MediaAttachment> GetMediaAttachments(Guid itemId);
+ /// <summary>
+ /// Gets the media attachments.
+ /// </summary>
+ /// <param name="">The The media source identifier.</param>
+ /// <returns>IEnumerable&lt;MediaAttachment&gt;.</returns>
+ List<MediaAttachment> GetMediaAttachments(string mediaSourceId);
+ /// <summary>
+ /// Gets the media attachments.
+ /// </summary>
+ /// <param name="">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..59c0a3f21
--- /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.MediaInfo;
+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..68df20c3a 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, List<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..d6106df29
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -0,0 +1,277 @@
+using System;
+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;
+ private readonly IProcessFactory _processFactory;
+
+ public AttachmentExtractor(
+ ILibraryManager libraryManager,
+ ILoggerFactory loggerFactory,
+ IApplicationPaths appPaths,
+ IFileSystem fileSystem,
+ IMediaEncoder mediaEncoder,
+ IMediaSourceManager mediaSourceManager,
+ IProcessFactory processFactory)
+ {
+ _libraryManager = libraryManager;
+ _logger = loggerFactory.CreateLogger(nameof(AttachmentExtractor));
+ _appPaths = appPaths;
+ _fileSystem = fileSystem;
+ _mediaEncoder = mediaEncoder;
+ _mediaSourceManager = mediaSourceManager;
+ _processFactory = processFactory;
+ }
+
+ 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));
+ }
+ if (string.IsNullOrWhiteSpace(mediaSourceId))
+ {
+ throw new ArgumentNullException(nameof(mediaSourceId));
+ }
+
+ var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(item, null, true, false, cancellationToken).ConfigureAwait(false);
+ var mediaSource = mediaSources
+ .First(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
+ var mediaAttachment = mediaSource.MediaAttachments
+ .First(i => i.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 inputFiles = new[] {mediaSource.Path};
+ var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource.Protocol, mediaAttachment, cancellationToken).ConfigureAwait(false);
+ var stream = await GetAttachmentStream(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(false);
+ return stream;
+ }
+
+ private async Task<Stream> GetAttachmentStream(
+ string path,
+ MediaProtocol protocol,
+ CancellationToken cancellationToken)
+ {
+ return File.OpenRead(path);
+ }
+
+ private async Task<AttachmentInfo> GetReadableFile(
+ 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 new AttachmentInfo(outputPath, MediaProtocol.File);
+ }
+
+ private struct AttachmentInfo
+ {
+ public AttachmentInfo(string path, MediaProtocol protocol)
+ {
+ Path = path;
+ Protocol = protocol;
+ }
+ public string Path { get; set; }
+ public MediaProtocol Protocol { get; set; }
+ }
+
+ 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("-dump_attachment:{1} {2} -i {0} -t 0 -f null null", inputPath, attachmentStreamIndex, outputPath);
+ var process = _processFactory.Create(new ProcessOptions
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ EnableRaisingEvents = true,
+ FileName = _mediaEncoder.EncoderPath,
+ Arguments = processArgs,
+ IsHidden = true,
+ ErrorDialog = false
+ });
+
+ _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
+
+ try
+ {
+ process.Start();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error starting ffmpeg");
+
+ throw;
+ }
+
+ var ranToCompletion = await process.WaitForExitAsync(300000).ConfigureAwait(false);
+
+ 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 == -1)
+ {
+ failed = true;
+
+ try
+ {
+ _logger.LogWarning("Deleting extracted attachment due to failure: {Path}", outputPath);
+ _fileSystem.DeleteFile(outputPath);
+ }
+ catch (FileNotFoundException)
+ {
+
+ }
+ 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
+ {
+ var msg = $"ffmpeg attachment extraction completed for {inputPath} to {outputPath}";
+
+ _logger.LogInformation(msg);
+ }
+ }
+
+ private string GetAttachmentCachePath(string mediaPath, MediaProtocol protocol, int attachmentStreamIndex)
+ {
+ if (protocol == MediaProtocol.File)
+ {
+ var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
+ var filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D");
+ var prefix = filename.Substring(0, 1);
+ return Path.Combine(AttachmentCachePath, prefix, filename);
+ }
+ else
+ {
+ var filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D");
+ var prefix = filename.Substring(0, 1);
+ return Path.Combine(AttachmentCachePath, prefix, filename);
+ }
+ }
+
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 54d02fc9f..2c001d775 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,40 @@ 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
+ };
+
+ // Filter out junk
+ if (!string.IsNullOrWhiteSpace(streamInfo.codec_tag_string) && streamInfo.codec_tag_string.IndexOf("[0]", StringComparison.OrdinalIgnoreCase) == -1)
+ {
+ 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.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
index 5bdc4809a..8a1aa55b6 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 List<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..26279b72b
--- /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..ae3e584d4 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;
+ List<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 = new List<MediaAttachment>();
chapters = new List<ChapterInfo>();
}
@@ -223,6 +226,7 @@ namespace MediaBrowser.Providers.MediaInfo
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)