Merge pull request #1838 from Unhelpful/media-attachments

Add support for embedded attachments in media files
pull/2240/head
dkanada 5 years ago committed by GitHub
commit 423d50e978
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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);

@ -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;
}
}
}

@ -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);

@ -96,7 +96,6 @@ namespace Jellyfin.Api.Controllers
public StartupUserDto GetFirstUser()
{
var user = _userManager.Users.First();
return new StartupUserDto
{
Name = user.Name,

@ -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);
}
}
}

@ -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)

@ -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,

@ -38,6 +38,20 @@ namespace MediaBrowser.Controller.Library
/// <returns>IEnumerable&lt;MediaStream&gt;.</returns>
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>

@ -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);
}
}

@ -78,6 +78,21 @@ namespace MediaBrowser.Controller.Persistence
/// <param name="cancellationToken">The cancellation token.</param>
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>

@ -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; }
}
}

@ -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;
}
}
}

@ -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);
@ -513,6 +517,39 @@ namespace MediaBrowser.MediaEncoding.Probing
return codec;
}
/// <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>

@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
</packages>

@ -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; }

@ -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; }
}
}

@ -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)

Loading…
Cancel
Save