mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-15 18:08:53 -07:00
improve direct play of live streams
This commit is contained in:
parent
8c61abf6d2
commit
2b7a80cfb5
@ -223,7 +223,7 @@ namespace MediaBrowser.Api.Playback
|
||||
int? subtitleStreamIndex,
|
||||
string playSessionId)
|
||||
{
|
||||
var streamBuilder = new StreamBuilder();
|
||||
var streamBuilder = new StreamBuilder(Logger);
|
||||
|
||||
var options = new VideoOptions
|
||||
{
|
||||
|
@ -211,7 +211,6 @@
|
||||
<Compile Include="MediaEncoding\IEncodingManager.cs" />
|
||||
<Compile Include="MediaEncoding\ImageEncodingOptions.cs" />
|
||||
<Compile Include="MediaEncoding\IMediaEncoder.cs" />
|
||||
<Compile Include="MediaEncoding\InternalMediaInfoResult.cs" />
|
||||
<Compile Include="MediaEncoding\ISubtitleEncoder.cs" />
|
||||
<Compile Include="MediaEncoding\MediaStreamSelector.cs" />
|
||||
<Compile Include="Net\AuthenticatedAttribute.cs" />
|
||||
|
@ -63,16 +63,18 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
string filenamePrefix,
|
||||
int? maxWidth,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the media info.
|
||||
/// </summary>
|
||||
/// <param name="inputFiles">The input files.</param>
|
||||
/// <param name="primaryPath">The primary path.</param>
|
||||
/// <param name="protocol">The protocol.</param>
|
||||
/// <param name="isAudio">if set to <c>true</c> [is audio].</param>
|
||||
/// <param name="extractChapters">if set to <c>true</c> [extract chapters].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task<InternalMediaInfoResult> GetMediaInfo(string[] inputFiles, MediaProtocol protocol, bool isAudio, CancellationToken cancellationToken);
|
||||
Task<MediaInfo> GetMediaInfo(string[] inputFiles, string primaryPath, MediaProtocol protocol, bool isAudio, bool extractChapters, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the probe size argument.
|
||||
|
@ -1,9 +1,7 @@
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
@ -46,291 +44,5 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
.Where(f => !string.IsNullOrEmpty(f))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static MediaInfo GetMediaInfo(InternalMediaInfoResult data)
|
||||
{
|
||||
var internalStreams = data.streams ?? new MediaStreamInfo[] { };
|
||||
|
||||
var info = new MediaInfo
|
||||
{
|
||||
MediaStreams = internalStreams.Select(s => GetMediaStream(s, data.format))
|
||||
.Where(i => i != null)
|
||||
.ToList()
|
||||
};
|
||||
|
||||
if (data.format != null)
|
||||
{
|
||||
info.Format = data.format.format_name;
|
||||
|
||||
if (!string.IsNullOrEmpty(data.format.bit_rate))
|
||||
{
|
||||
info.TotalBitrate = int.Parse(data.format.bit_rate, UsCulture);
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
||||
/// <summary>
|
||||
/// Converts ffprobe stream info to our MediaStream class
|
||||
/// </summary>
|
||||
/// <param name="streamInfo">The stream info.</param>
|
||||
/// <param name="formatInfo">The format info.</param>
|
||||
/// <returns>MediaStream.</returns>
|
||||
private static MediaStream GetMediaStream(MediaStreamInfo streamInfo, MediaFormatInfo formatInfo)
|
||||
{
|
||||
var stream = new MediaStream
|
||||
{
|
||||
Codec = streamInfo.codec_name,
|
||||
Profile = streamInfo.profile,
|
||||
Level = streamInfo.level,
|
||||
Index = streamInfo.index,
|
||||
PixelFormat = streamInfo.pix_fmt
|
||||
};
|
||||
|
||||
if (streamInfo.tags != null)
|
||||
{
|
||||
stream.Language = GetDictionaryValue(streamInfo.tags, "language");
|
||||
}
|
||||
|
||||
if (string.Equals(streamInfo.codec_type, "audio", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = MediaStreamType.Audio;
|
||||
|
||||
stream.Channels = streamInfo.channels;
|
||||
|
||||
if (!string.IsNullOrEmpty(streamInfo.sample_rate))
|
||||
{
|
||||
stream.SampleRate = int.Parse(streamInfo.sample_rate, UsCulture);
|
||||
}
|
||||
|
||||
stream.ChannelLayout = ParseChannelLayout(streamInfo.channel_layout);
|
||||
}
|
||||
else if (string.Equals(streamInfo.codec_type, "subtitle", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = MediaStreamType.Subtitle;
|
||||
}
|
||||
else if (string.Equals(streamInfo.codec_type, "video", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = (streamInfo.codec_name ?? string.Empty).IndexOf("mjpeg", StringComparison.OrdinalIgnoreCase) != -1
|
||||
? MediaStreamType.EmbeddedImage
|
||||
: MediaStreamType.Video;
|
||||
|
||||
stream.Width = streamInfo.width;
|
||||
stream.Height = streamInfo.height;
|
||||
stream.AspectRatio = GetAspectRatio(streamInfo);
|
||||
|
||||
stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate);
|
||||
stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate);
|
||||
|
||||
stream.BitDepth = GetBitDepth(stream.PixelFormat);
|
||||
|
||||
//stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) ||
|
||||
// string.Equals(stream.AspectRatio, "2.35:1", StringComparison.OrdinalIgnoreCase) ||
|
||||
// string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get stream bitrate
|
||||
var bitrate = 0;
|
||||
|
||||
if (!string.IsNullOrEmpty(streamInfo.bit_rate))
|
||||
{
|
||||
bitrate = int.Parse(streamInfo.bit_rate, UsCulture);
|
||||
}
|
||||
else if (formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate) && stream.Type == MediaStreamType.Video)
|
||||
{
|
||||
// If the stream info doesn't have a bitrate get the value from the media format info
|
||||
bitrate = int.Parse(formatInfo.bit_rate, UsCulture);
|
||||
}
|
||||
|
||||
if (bitrate > 0)
|
||||
{
|
||||
stream.BitRate = bitrate;
|
||||
}
|
||||
|
||||
if (streamInfo.disposition != null)
|
||||
{
|
||||
var isDefault = GetDictionaryValue(streamInfo.disposition, "default");
|
||||
var isForced = GetDictionaryValue(streamInfo.disposition, "forced");
|
||||
|
||||
stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
private static int? GetBitDepth(string pixelFormat)
|
||||
{
|
||||
var eightBit = new List<string>
|
||||
{
|
||||
"yuv420p",
|
||||
"yuv411p",
|
||||
"yuvj420p",
|
||||
"uyyvyy411",
|
||||
"nv12",
|
||||
"nv21",
|
||||
"rgb444le",
|
||||
"rgb444be",
|
||||
"bgr444le",
|
||||
"bgr444be",
|
||||
"yuvj411p"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(pixelFormat))
|
||||
{
|
||||
if (eightBit.Contains(pixelFormat, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string from an FFProbeResult tags dictionary
|
||||
/// </summary>
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private static string GetDictionaryValue(Dictionary<string, string> tags, string key)
|
||||
{
|
||||
if (tags == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string val;
|
||||
|
||||
tags.TryGetValue(key, out val);
|
||||
return val;
|
||||
}
|
||||
|
||||
private static string ParseChannelLayout(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
return input.Split('(').FirstOrDefault();
|
||||
}
|
||||
|
||||
private static string GetAspectRatio(MediaStreamInfo info)
|
||||
{
|
||||
var original = info.display_aspect_ratio;
|
||||
|
||||
int height;
|
||||
int width;
|
||||
|
||||
var parts = (original ?? string.Empty).Split(':');
|
||||
if (!(parts.Length == 2 &&
|
||||
int.TryParse(parts[0], NumberStyles.Any, UsCulture, out width) &&
|
||||
int.TryParse(parts[1], NumberStyles.Any, UsCulture, out height) &&
|
||||
width > 0 &&
|
||||
height > 0))
|
||||
{
|
||||
width = info.width;
|
||||
height = info.height;
|
||||
}
|
||||
|
||||
if (width > 0 && height > 0)
|
||||
{
|
||||
double ratio = width;
|
||||
ratio /= height;
|
||||
|
||||
if (IsClose(ratio, 1.777777778, .03))
|
||||
{
|
||||
return "16:9";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.3333333333, .05))
|
||||
{
|
||||
return "4:3";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.41))
|
||||
{
|
||||
return "1.41:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.5))
|
||||
{
|
||||
return "1.5:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.6))
|
||||
{
|
||||
return "1.6:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.66666666667))
|
||||
{
|
||||
return "5:3";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.85, .02))
|
||||
{
|
||||
return "1.85:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 2.35, .025))
|
||||
{
|
||||
return "2.35:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 2.4, .025))
|
||||
{
|
||||
return "2.40:1";
|
||||
}
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
private static bool IsClose(double d1, double d2, double variance = .005)
|
||||
{
|
||||
return Math.Abs(d1 - d2) <= variance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a frame rate from a string value in ffprobe output
|
||||
/// This could be a number or in the format of 2997/125.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>System.Nullable{System.Single}.</returns>
|
||||
private static float? GetFrameRate(string value)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
var parts = value.Split('/');
|
||||
|
||||
float result;
|
||||
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
result = float.Parse(parts[0], UsCulture) / float.Parse(parts[1], UsCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = float.Parse(parts[0], UsCulture);
|
||||
}
|
||||
|
||||
return float.IsNaN(result) ? (float?)null : result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ using MediaBrowser.Dlna.ContentDirectory;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Net;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
@ -126,7 +127,7 @@ namespace MediaBrowser.Dlna.Didl
|
||||
{
|
||||
var sources = _mediaSourceManager.GetStaticMediaSources(video, true, _user).ToList();
|
||||
|
||||
streamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions
|
||||
streamInfo = new StreamBuilder(new NullLogger()).BuildVideoItem(new VideoOptions
|
||||
{
|
||||
ItemId = GetClientId(video),
|
||||
MediaSources = sources,
|
||||
@ -353,7 +354,7 @@ namespace MediaBrowser.Dlna.Didl
|
||||
{
|
||||
var sources = _mediaSourceManager.GetStaticMediaSources(audio, true, _user).ToList();
|
||||
|
||||
streamInfo = new StreamBuilder().BuildAudioItem(new AudioOptions
|
||||
streamInfo = new StreamBuilder(new NullLogger()).BuildAudioItem(new AudioOptions
|
||||
{
|
||||
ItemId = GetClientId(audio),
|
||||
MediaSources = sources,
|
||||
|
@ -542,7 +542,7 @@ namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
return new PlaylistItem
|
||||
{
|
||||
StreamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions
|
||||
StreamInfo = new StreamBuilder(_logger).BuildVideoItem(new VideoOptions
|
||||
{
|
||||
ItemId = item.Id.ToString("N"),
|
||||
MediaSources = mediaSources,
|
||||
@ -562,7 +562,7 @@ namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
return new PlaylistItem
|
||||
{
|
||||
StreamInfo = new StreamBuilder().BuildAudioItem(new AudioOptions
|
||||
StreamInfo = new StreamBuilder(_logger).BuildAudioItem(new AudioOptions
|
||||
{
|
||||
ItemId = item.Id.ToString("N"),
|
||||
MediaSources = mediaSources,
|
||||
|
@ -5,6 +5,7 @@ using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.MediaEncoding.Probing;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Logging;
|
||||
@ -103,15 +104,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
/// Gets the media info.
|
||||
/// </summary>
|
||||
/// <param name="inputFiles">The input files.</param>
|
||||
/// <param name="primaryPath">The primary path.</param>
|
||||
/// <param name="protocol">The protocol.</param>
|
||||
/// <param name="isAudio">if set to <c>true</c> [is audio].</param>
|
||||
/// <param name="extractChapters">if set to <c>true</c> [extract chapters].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task<InternalMediaInfoResult> GetMediaInfo(string[] inputFiles, MediaProtocol protocol, bool isAudio,
|
||||
CancellationToken cancellationToken)
|
||||
public Task<Model.Entities.MediaInfo> GetMediaInfo(string[] inputFiles, string primaryPath, MediaProtocol protocol, bool isAudio,
|
||||
bool extractChapters, CancellationToken cancellationToken)
|
||||
{
|
||||
return GetMediaInfoInternal(GetInputArgument(inputFiles, protocol), !isAudio,
|
||||
GetProbeSizeArgument(inputFiles, protocol), cancellationToken);
|
||||
return GetMediaInfoInternal(GetInputArgument(inputFiles, protocol), primaryPath, protocol, !isAudio && extractChapters,
|
||||
GetProbeSizeArgument(inputFiles, protocol), isAudio, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -141,13 +144,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
/// Gets the media info internal.
|
||||
/// </summary>
|
||||
/// <param name="inputPath">The input path.</param>
|
||||
/// <param name="primaryPath">The primary path.</param>
|
||||
/// <param name="protocol">The protocol.</param>
|
||||
/// <param name="extractChapters">if set to <c>true</c> [extract chapters].</param>
|
||||
/// <param name="probeSizeArgument">The probe size argument.</param>
|
||||
/// <param name="isAudio">if set to <c>true</c> [is audio].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{MediaInfoResult}.</returns>
|
||||
/// <exception cref="System.ApplicationException"></exception>
|
||||
private async Task<InternalMediaInfoResult> GetMediaInfoInternal(string inputPath, bool extractChapters,
|
||||
private async Task<Model.Entities.MediaInfo> GetMediaInfoInternal(string inputPath, string primaryPath, MediaProtocol protocol, bool extractChapters,
|
||||
string probeSizeArgument,
|
||||
bool isAudio,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var args = extractChapters
|
||||
@ -244,7 +251,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return new ProbeResultNormalizer(_logger, FileSystem).GetMediaInfo(result, isAudio, primaryPath, protocol);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -68,6 +68,9 @@
|
||||
<Compile Include="Encoder\JobLogger.cs" />
|
||||
<Compile Include="Encoder\MediaEncoder.cs" />
|
||||
<Compile Include="Encoder\VideoEncoder.cs" />
|
||||
<Compile Include="Probing\FFProbeHelpers.cs" />
|
||||
<Compile Include="Probing\InternalMediaInfoResult.cs" />
|
||||
<Compile Include="Probing\ProbeResultNormalizer.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Subtitles\ISubtitleParser.cs" />
|
||||
<Compile Include="Subtitles\ISubtitleWriter.cs" />
|
||||
@ -91,6 +94,10 @@
|
||||
<Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
|
||||
<Name>MediaBrowser.Controller</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\MediaBrowser.MediaInfo\MediaBrowser.MediaInfo.csproj">
|
||||
<Project>{6e4145e4-c6d4-4e4d-94f2-87188db6e239}</Project>
|
||||
<Name>MediaBrowser.MediaInfo</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
|
||||
<Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
|
||||
<Name>MediaBrowser.Model</Name>
|
||||
|
@ -2,7 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Providers.MediaInfo
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
public static class FFProbeHelpers
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MediaInfoResult
|
||||
@ -89,7 +89,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
/// </summary>
|
||||
/// <value>The channel_layout.</value>
|
||||
public string channel_layout { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the avg_frame_rate.
|
||||
/// </summary>
|
||||
@ -317,7 +317,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
/// </summary>
|
||||
/// <value>The probe_score.</value>
|
||||
public int probe_score { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tags.
|
||||
/// </summary>
|
882
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
Normal file
882
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
Normal file
@ -0,0 +1,882 @@
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.MediaInfo;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
public class ProbeResultNormalizer
|
||||
{
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
private readonly ILogger _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public ProbeResultNormalizer(ILogger logger, IFileSystem fileSystem)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
public Model.Entities.MediaInfo GetMediaInfo(InternalMediaInfoResult data, bool isAudio, string path, MediaProtocol protocol)
|
||||
{
|
||||
var info = new Model.Entities.MediaInfo
|
||||
{
|
||||
Path = path,
|
||||
Protocol = protocol
|
||||
};
|
||||
|
||||
FFProbeHelpers.NormalizeFFProbeResult(data);
|
||||
SetSize(data, info);
|
||||
|
||||
var internalStreams = data.streams ?? new MediaStreamInfo[] { };
|
||||
|
||||
info.MediaStreams = internalStreams.Select(s => GetMediaStream(s, data.format))
|
||||
.Where(i => i != null)
|
||||
.ToList();
|
||||
|
||||
if (data.format != null)
|
||||
{
|
||||
info.Container = data.format.format_name;
|
||||
|
||||
if (!string.IsNullOrEmpty(data.format.bit_rate))
|
||||
{
|
||||
info.Bitrate = int.Parse(data.format.bit_rate, _usCulture);
|
||||
}
|
||||
}
|
||||
|
||||
if (isAudio)
|
||||
{
|
||||
SetAudioRuntimeTicks(data, info);
|
||||
|
||||
if (data.format != null && data.format.tags != null)
|
||||
{
|
||||
SetAudioInfoFromTags(info, data.format.tags);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FetchWtvInfo(info, data);
|
||||
|
||||
if (data.Chapters != null)
|
||||
{
|
||||
info.Chapters = data.Chapters.Select(GetChapterInfo).ToList();
|
||||
}
|
||||
|
||||
ExtractTimestamp(info);
|
||||
|
||||
var videoStream = info.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
|
||||
|
||||
if (videoStream != null)
|
||||
{
|
||||
UpdateFromMediaInfo(info, videoStream);
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts ffprobe stream info to our MediaStream class
|
||||
/// </summary>
|
||||
/// <param name="streamInfo">The stream info.</param>
|
||||
/// <param name="formatInfo">The format info.</param>
|
||||
/// <returns>MediaStream.</returns>
|
||||
private MediaStream GetMediaStream(MediaStreamInfo streamInfo, MediaFormatInfo formatInfo)
|
||||
{
|
||||
var stream = new MediaStream
|
||||
{
|
||||
Codec = streamInfo.codec_name,
|
||||
Profile = streamInfo.profile,
|
||||
Level = streamInfo.level,
|
||||
Index = streamInfo.index,
|
||||
PixelFormat = streamInfo.pix_fmt
|
||||
};
|
||||
|
||||
if (streamInfo.tags != null)
|
||||
{
|
||||
stream.Language = GetDictionaryValue(streamInfo.tags, "language");
|
||||
}
|
||||
|
||||
if (string.Equals(streamInfo.codec_type, "audio", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = MediaStreamType.Audio;
|
||||
|
||||
stream.Channels = streamInfo.channels;
|
||||
|
||||
if (!string.IsNullOrEmpty(streamInfo.sample_rate))
|
||||
{
|
||||
stream.SampleRate = int.Parse(streamInfo.sample_rate, _usCulture);
|
||||
}
|
||||
|
||||
stream.ChannelLayout = ParseChannelLayout(streamInfo.channel_layout);
|
||||
}
|
||||
else if (string.Equals(streamInfo.codec_type, "subtitle", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = MediaStreamType.Subtitle;
|
||||
}
|
||||
else if (string.Equals(streamInfo.codec_type, "video", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = (streamInfo.codec_name ?? string.Empty).IndexOf("mjpeg", StringComparison.OrdinalIgnoreCase) != -1
|
||||
? MediaStreamType.EmbeddedImage
|
||||
: MediaStreamType.Video;
|
||||
|
||||
stream.Width = streamInfo.width;
|
||||
stream.Height = streamInfo.height;
|
||||
stream.AspectRatio = GetAspectRatio(streamInfo);
|
||||
|
||||
stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate);
|
||||
stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate);
|
||||
|
||||
stream.BitDepth = GetBitDepth(stream.PixelFormat);
|
||||
|
||||
//stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) ||
|
||||
// string.Equals(stream.AspectRatio, "2.35:1", StringComparison.OrdinalIgnoreCase) ||
|
||||
// string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get stream bitrate
|
||||
var bitrate = 0;
|
||||
|
||||
if (!string.IsNullOrEmpty(streamInfo.bit_rate))
|
||||
{
|
||||
bitrate = int.Parse(streamInfo.bit_rate, _usCulture);
|
||||
}
|
||||
else if (formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate) && stream.Type == MediaStreamType.Video)
|
||||
{
|
||||
// If the stream info doesn't have a bitrate get the value from the media format info
|
||||
bitrate = int.Parse(formatInfo.bit_rate, _usCulture);
|
||||
}
|
||||
|
||||
if (bitrate > 0)
|
||||
{
|
||||
stream.BitRate = bitrate;
|
||||
}
|
||||
|
||||
if (streamInfo.disposition != null)
|
||||
{
|
||||
var isDefault = GetDictionaryValue(streamInfo.disposition, "default");
|
||||
var isForced = GetDictionaryValue(streamInfo.disposition, "forced");
|
||||
|
||||
stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
private int? GetBitDepth(string pixelFormat)
|
||||
{
|
||||
var eightBit = new List<string>
|
||||
{
|
||||
"yuv420p",
|
||||
"yuv411p",
|
||||
"yuvj420p",
|
||||
"uyyvyy411",
|
||||
"nv12",
|
||||
"nv21",
|
||||
"rgb444le",
|
||||
"rgb444be",
|
||||
"bgr444le",
|
||||
"bgr444be",
|
||||
"yuvj411p"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(pixelFormat))
|
||||
{
|
||||
if (eightBit.Contains(pixelFormat, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string from an FFProbeResult tags dictionary
|
||||
/// </summary>
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetDictionaryValue(Dictionary<string, string> tags, string key)
|
||||
{
|
||||
if (tags == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string val;
|
||||
|
||||
tags.TryGetValue(key, out val);
|
||||
return val;
|
||||
}
|
||||
|
||||
private string ParseChannelLayout(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
return input.Split('(').FirstOrDefault();
|
||||
}
|
||||
|
||||
private string GetAspectRatio(MediaStreamInfo info)
|
||||
{
|
||||
var original = info.display_aspect_ratio;
|
||||
|
||||
int height;
|
||||
int width;
|
||||
|
||||
var parts = (original ?? string.Empty).Split(':');
|
||||
if (!(parts.Length == 2 &&
|
||||
int.TryParse(parts[0], NumberStyles.Any, _usCulture, out width) &&
|
||||
int.TryParse(parts[1], NumberStyles.Any, _usCulture, out height) &&
|
||||
width > 0 &&
|
||||
height > 0))
|
||||
{
|
||||
width = info.width;
|
||||
height = info.height;
|
||||
}
|
||||
|
||||
if (width > 0 && height > 0)
|
||||
{
|
||||
double ratio = width;
|
||||
ratio /= height;
|
||||
|
||||
if (IsClose(ratio, 1.777777778, .03))
|
||||
{
|
||||
return "16:9";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.3333333333, .05))
|
||||
{
|
||||
return "4:3";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.41))
|
||||
{
|
||||
return "1.41:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.5))
|
||||
{
|
||||
return "1.5:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.6))
|
||||
{
|
||||
return "1.6:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.66666666667))
|
||||
{
|
||||
return "5:3";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.85, .02))
|
||||
{
|
||||
return "1.85:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 2.35, .025))
|
||||
{
|
||||
return "2.35:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 2.4, .025))
|
||||
{
|
||||
return "2.40:1";
|
||||
}
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
private bool IsClose(double d1, double d2, double variance = .005)
|
||||
{
|
||||
return Math.Abs(d1 - d2) <= variance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a frame rate from a string value in ffprobe output
|
||||
/// This could be a number or in the format of 2997/125.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>System.Nullable{System.Single}.</returns>
|
||||
private float? GetFrameRate(string value)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
var parts = value.Split('/');
|
||||
|
||||
float result;
|
||||
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
result = float.Parse(parts[0], _usCulture) / float.Parse(parts[1], _usCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = float.Parse(parts[0], _usCulture);
|
||||
}
|
||||
|
||||
return float.IsNaN(result) ? (float?)null : result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void SetAudioRuntimeTicks(InternalMediaInfoResult result, Model.Entities.MediaInfo data)
|
||||
{
|
||||
if (result.streams != null)
|
||||
{
|
||||
// Get the first audio stream
|
||||
var stream = result.streams.FirstOrDefault(s => string.Equals(s.codec_type, "audio", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (stream != null)
|
||||
{
|
||||
// Get duration from stream properties
|
||||
var duration = stream.duration;
|
||||
|
||||
// If it's not there go into format properties
|
||||
if (string.IsNullOrEmpty(duration))
|
||||
{
|
||||
duration = result.format.duration;
|
||||
}
|
||||
|
||||
// If we got something, parse it
|
||||
if (!string.IsNullOrEmpty(duration))
|
||||
{
|
||||
data.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration, _usCulture)).Ticks;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetSize(InternalMediaInfoResult data, Model.Entities.MediaInfo info)
|
||||
{
|
||||
if (data.format != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(data.format.size))
|
||||
{
|
||||
info.Size = long.Parse(data.format.size, _usCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
info.Size = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetAudioInfoFromTags(Model.Entities.MediaInfo audio, Dictionary<string, string> tags)
|
||||
{
|
||||
var title = FFProbeHelpers.GetDictionaryValue(tags, "title");
|
||||
|
||||
// Only set Name if title was found in the dictionary
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
audio.Title = title;
|
||||
}
|
||||
|
||||
var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(composer))
|
||||
{
|
||||
foreach (var person in Split(composer, false))
|
||||
{
|
||||
audio.People.Add(new BaseItemPerson { Name = person, Type = PersonType.Composer });
|
||||
}
|
||||
}
|
||||
|
||||
audio.Album = FFProbeHelpers.GetDictionaryValue(tags, "album");
|
||||
|
||||
var artists = FFProbeHelpers.GetDictionaryValue(tags, "artists");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(artists))
|
||||
{
|
||||
audio.Artists = artists.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist");
|
||||
if (string.IsNullOrWhiteSpace(artist))
|
||||
{
|
||||
audio.Artists.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
audio.Artists = SplitArtists(artist)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
var albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "albumartist");
|
||||
if (string.IsNullOrWhiteSpace(albumArtist))
|
||||
{
|
||||
albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "album artist");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(albumArtist))
|
||||
{
|
||||
albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "album_artist");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(albumArtist))
|
||||
{
|
||||
audio.AlbumArtists = new List<string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
audio.AlbumArtists = SplitArtists(albumArtist)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
}
|
||||
|
||||
// Track number
|
||||
audio.IndexNumber = GetDictionaryDiscValue(tags, "track");
|
||||
|
||||
// Disc number
|
||||
audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc");
|
||||
|
||||
audio.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date");
|
||||
|
||||
// Several different forms of retaildate
|
||||
audio.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ??
|
||||
FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ??
|
||||
FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ??
|
||||
FFProbeHelpers.GetDictionaryDateTime(tags, "date");
|
||||
|
||||
// If we don't have a ProductionYear try and get it from PremiereDate
|
||||
if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue)
|
||||
{
|
||||
audio.ProductionYear = audio.PremiereDate.Value.ToLocalTime().Year;
|
||||
}
|
||||
|
||||
FetchGenres(audio, tags);
|
||||
|
||||
// There's several values in tags may or may not be present
|
||||
FetchStudios(audio, tags, "organization");
|
||||
FetchStudios(audio, tags, "ensemble");
|
||||
FetchStudios(audio, tags, "publisher");
|
||||
|
||||
// These support mulitple values, but for now we only store the first.
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzAlbumArtist, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Artist Id")));
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzArtist, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Artist Id")));
|
||||
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzAlbum, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Id")));
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzReleaseGroup, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Group Id")));
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzTrack, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Track Id")));
|
||||
}
|
||||
|
||||
private string GetMultipleMusicBrainzId(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(i => i.Trim())
|
||||
.FirstOrDefault(i => !string.IsNullOrWhiteSpace(i));
|
||||
}
|
||||
|
||||
private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
|
||||
|
||||
/// <summary>
|
||||
/// Splits the specified val.
|
||||
/// </summary>
|
||||
/// <param name="val">The val.</param>
|
||||
/// <param name="allowCommaDelimiter">if set to <c>true</c> [allow comma delimiter].</param>
|
||||
/// <returns>System.String[][].</returns>
|
||||
private IEnumerable<string> Split(string val, bool allowCommaDelimiter)
|
||||
{
|
||||
// Only use the comma as a delimeter if there are no slashes or pipes.
|
||||
// We want to be careful not to split names that have commas in them
|
||||
var delimeter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.IndexOf(i) != -1) ?
|
||||
_nameDelimiters :
|
||||
new[] { ',' };
|
||||
|
||||
return val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => i.Trim());
|
||||
}
|
||||
|
||||
private const string ArtistReplaceValue = " | ";
|
||||
|
||||
private IEnumerable<string> SplitArtists(string val)
|
||||
{
|
||||
val = val.Replace(" featuring ", ArtistReplaceValue, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(" feat. ", ArtistReplaceValue, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var artistsFound = new List<string>();
|
||||
|
||||
foreach (var whitelistArtist in GetSplitWhitelist())
|
||||
{
|
||||
var originalVal = val;
|
||||
val = val.Replace(whitelistArtist, "|", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!string.Equals(originalVal, val, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
artistsFound.Add(whitelistArtist);
|
||||
}
|
||||
}
|
||||
|
||||
// Only use the comma as a delimeter if there are no slashes or pipes.
|
||||
// We want to be careful not to split names that have commas in them
|
||||
var delimeter = _nameDelimiters;
|
||||
|
||||
var artists = val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => i.Trim());
|
||||
|
||||
artistsFound.AddRange(artists);
|
||||
return artistsFound;
|
||||
}
|
||||
|
||||
|
||||
private List<string> _splitWhiteList = null;
|
||||
|
||||
private IEnumerable<string> GetSplitWhitelist()
|
||||
{
|
||||
if (_splitWhiteList == null)
|
||||
{
|
||||
var file = GetType().Namespace + ".whitelist.txt";
|
||||
|
||||
using (var stream = GetType().Assembly.GetManifestResourceStream(file))
|
||||
{
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
var list = new List<string>();
|
||||
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
var val = reader.ReadLine();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
list.Add(val);
|
||||
}
|
||||
}
|
||||
|
||||
_splitWhiteList = list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _splitWhiteList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the studios from the tags collection
|
||||
/// </summary>
|
||||
/// <param name="audio">The audio.</param>
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="tagName">Name of the tag.</param>
|
||||
private void FetchStudios(Model.Entities.MediaInfo audio, Dictionary<string, string> tags, string tagName)
|
||||
{
|
||||
var val = FFProbeHelpers.GetDictionaryValue(tags, tagName);
|
||||
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
var studios = Split(val, true);
|
||||
|
||||
foreach (var studio in studios)
|
||||
{
|
||||
// Sometimes the artist name is listed here, account for that
|
||||
if (audio.Artists.Contains(studio, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (audio.AlbumArtists.Contains(studio, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
audio.Studios.Add(studio);
|
||||
}
|
||||
|
||||
audio.Studios = audio.Studios
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the genres from the tags collection
|
||||
/// </summary>
|
||||
/// <param name="info">The information.</param>
|
||||
/// <param name="tags">The tags.</param>
|
||||
private void FetchGenres(Model.Entities.MediaInfo info, Dictionary<string, string> tags)
|
||||
{
|
||||
var val = FFProbeHelpers.GetDictionaryValue(tags, "genre");
|
||||
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
foreach (var genre in Split(val, true))
|
||||
{
|
||||
info.Genres.Add(genre);
|
||||
}
|
||||
|
||||
info.Genres = info.Genres
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the disc number, which is sometimes can be in the form of '1', or '1/3'
|
||||
/// </summary>
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="tagName">Name of the tag.</param>
|
||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||
private int? GetDictionaryDiscValue(Dictionary<string, string> tags, string tagName)
|
||||
{
|
||||
var disc = FFProbeHelpers.GetDictionaryValue(tags, tagName);
|
||||
|
||||
if (!string.IsNullOrEmpty(disc))
|
||||
{
|
||||
disc = disc.Split('/')[0];
|
||||
|
||||
int num;
|
||||
|
||||
if (int.TryParse(disc, out num))
|
||||
{
|
||||
return num;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private ChapterInfo GetChapterInfo(MediaChapter chapter)
|
||||
{
|
||||
var info = new ChapterInfo();
|
||||
|
||||
if (chapter.tags != null)
|
||||
{
|
||||
string name;
|
||||
if (chapter.tags.TryGetValue("title", out name))
|
||||
{
|
||||
info.Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
// Limit accuracy to milliseconds to match xml saving
|
||||
var secondsString = chapter.start_time;
|
||||
double seconds;
|
||||
|
||||
if (double.TryParse(secondsString, NumberStyles.Any, CultureInfo.InvariantCulture, out seconds))
|
||||
{
|
||||
var ms = Math.Round(TimeSpan.FromSeconds(seconds).TotalMilliseconds);
|
||||
info.StartPositionTicks = TimeSpan.FromMilliseconds(ms).Ticks;
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private const int MaxSubtitleDescriptionExtractionLength = 100; // When extracting subtitles, the maximum length to consider (to avoid invalid filenames)
|
||||
|
||||
private void FetchWtvInfo(Model.Entities.MediaInfo video, InternalMediaInfoResult data)
|
||||
{
|
||||
if (data.format == null || data.format.tags == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/Genre");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(genres))
|
||||
{
|
||||
//genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "genre");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(genres))
|
||||
{
|
||||
video.Genres = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => i.Trim())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var officialRating = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/ParentalRating");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(officialRating))
|
||||
{
|
||||
video.OfficialRating = officialRating;
|
||||
}
|
||||
|
||||
var people = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaCredits");
|
||||
|
||||
if (!string.IsNullOrEmpty(people))
|
||||
{
|
||||
video.People = people.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => new BaseItemPerson { Name = i.Trim(), Type = PersonType.Actor })
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var year = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/OriginalReleaseTime");
|
||||
if (!string.IsNullOrWhiteSpace(year))
|
||||
{
|
||||
int val;
|
||||
|
||||
if (int.TryParse(year, NumberStyles.Integer, _usCulture, out val))
|
||||
{
|
||||
video.ProductionYear = val;
|
||||
}
|
||||
}
|
||||
|
||||
var premiereDateString = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaOriginalBroadcastDateTime");
|
||||
if (!string.IsNullOrWhiteSpace(premiereDateString))
|
||||
{
|
||||
DateTime val;
|
||||
|
||||
// Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
|
||||
// DateTime is reported along with timezone info (typically Z i.e. UTC hence assume None)
|
||||
if (DateTime.TryParse(year, null, DateTimeStyles.None, out val))
|
||||
{
|
||||
video.PremiereDate = val.ToUniversalTime();
|
||||
}
|
||||
}
|
||||
|
||||
var description = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitleDescription");
|
||||
|
||||
var subTitle = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitle");
|
||||
|
||||
// For below code, credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
|
||||
|
||||
// Sometimes for TV Shows the Subtitle field is empty and the subtitle description contains the subtitle, extract if possible. See ticket https://mcebuddy2x.codeplex.com/workitem/1910
|
||||
// The format is -> EPISODE/TOTAL_EPISODES_IN_SEASON. SUBTITLE: DESCRIPTION
|
||||
// OR -> COMMENT. SUBTITLE: DESCRIPTION
|
||||
// e.g. -> 4/13. The Doctor's Wife: Science fiction drama. When he follows a Time Lord distress signal, the Doctor puts Amy, Rory and his beloved TARDIS in grave danger. Also in HD. [AD,S]
|
||||
// e.g. -> CBeebies Bedtime Hour. The Mystery: Animated adventures of two friends who live on an island in the middle of the big city. Some of Abney and Teal's favourite objects are missing. [S]
|
||||
if (String.IsNullOrWhiteSpace(subTitle) && !String.IsNullOrWhiteSpace(description) && description.Substring(0, Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)).Contains(":")) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
|
||||
{
|
||||
string[] parts = description.Split(':');
|
||||
if (parts.Length > 0)
|
||||
{
|
||||
string subtitle = parts[0];
|
||||
try
|
||||
{
|
||||
if (subtitle.Contains("/")) // It contains a episode number and season number
|
||||
{
|
||||
string[] numbers = subtitle.Split(' ');
|
||||
video.IndexNumber = int.Parse(numbers[0].Replace(".", "").Split('/')[0]);
|
||||
int totalEpisodesInSeason = int.Parse(numbers[0].Replace(".", "").Split('/')[1]);
|
||||
|
||||
description = String.Join(" ", numbers, 1, numbers.Length - 1).Trim(); // Skip the first, concatenate the rest, clean up spaces and save it
|
||||
}
|
||||
else
|
||||
throw new Exception(); // Switch to default parsing
|
||||
}
|
||||
catch // Default parsing
|
||||
{
|
||||
if (subtitle.Contains(".")) // skip the comment, keep the subtitle
|
||||
description = String.Join(".", subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first
|
||||
else
|
||||
description = subtitle.Trim(); // Clean up whitespaces and save it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
video.Overview = description;
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtractTimestamp(Model.Entities.MediaInfo video)
|
||||
{
|
||||
if (video.VideoType == VideoType.VideoFile)
|
||||
{
|
||||
if (string.Equals(video.Container, "mpeg2ts", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(video.Container, "m2ts", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(video.Container, "ts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
video.Timestamp = GetMpegTimestamp(video.Path);
|
||||
|
||||
_logger.Debug("Video has {0} timestamp", video.Timestamp);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error extracting timestamp info from {0}", ex, video.Path);
|
||||
video.Timestamp = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TransportStreamTimestamp GetMpegTimestamp(string path)
|
||||
{
|
||||
var packetBuffer = new byte['Å'];
|
||||
|
||||
using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
fs.Read(packetBuffer, 0, packetBuffer.Length);
|
||||
}
|
||||
|
||||
if (packetBuffer[0] == 71)
|
||||
{
|
||||
return TransportStreamTimestamp.None;
|
||||
}
|
||||
|
||||
if ((packetBuffer[4] == 71) && (packetBuffer['Ä'] == 71))
|
||||
{
|
||||
if ((packetBuffer[0] == 0) && (packetBuffer[1] == 0) && (packetBuffer[2] == 0) && (packetBuffer[3] == 0))
|
||||
{
|
||||
return TransportStreamTimestamp.Zero;
|
||||
}
|
||||
|
||||
return TransportStreamTimestamp.Valid;
|
||||
}
|
||||
|
||||
return TransportStreamTimestamp.None;
|
||||
}
|
||||
|
||||
private void UpdateFromMediaInfo(MediaSourceInfo video, MediaStream videoStream)
|
||||
{
|
||||
if (video.VideoType == VideoType.VideoFile && video.Protocol == MediaProtocol.File)
|
||||
{
|
||||
if (videoStream != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = new MediaInfoLib().GetVideoInfo(video.Path);
|
||||
|
||||
videoStream.IsCabac = result.IsCabac ?? videoStream.IsCabac;
|
||||
videoStream.IsInterlaced = result.IsInterlaced ?? videoStream.IsInterlaced;
|
||||
videoStream.BitDepth = result.BitDepth ?? videoStream.BitDepth;
|
||||
videoStream.RefFrames = result.RefFrames;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error running MediaInfo on {0}", ex, video.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Session;
|
||||
using System;
|
||||
@ -11,13 +12,16 @@ namespace MediaBrowser.Model.Dlna
|
||||
public class StreamBuilder
|
||||
{
|
||||
private readonly ILocalPlayer _localPlayer;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public StreamBuilder(ILocalPlayer localPlayer)
|
||||
public StreamBuilder(ILocalPlayer localPlayer, ILogger logger)
|
||||
{
|
||||
_localPlayer = localPlayer;
|
||||
_logger = logger;
|
||||
}
|
||||
public StreamBuilder()
|
||||
: this(new NullLocalPlayer())
|
||||
|
||||
public StreamBuilder(ILogger logger)
|
||||
: this(new NullLocalPlayer(), logger)
|
||||
{
|
||||
}
|
||||
|
||||
@ -353,6 +357,12 @@ namespace MediaBrowser.Model.Dlna
|
||||
bool isEligibleForDirectPlay = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options), subtitleStream, options);
|
||||
bool isEligibleForDirectStream = IsEligibleForDirectPlay(item, options.GetMaxBitrate(), subtitleStream, options);
|
||||
|
||||
_logger.Debug("Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
|
||||
options.Profile.Name ?? "Unknown Profile",
|
||||
item.Path ?? "Unknown path",
|
||||
isEligibleForDirectPlay,
|
||||
isEligibleForDirectStream);
|
||||
|
||||
if (isEligibleForDirectPlay || isEligibleForDirectStream)
|
||||
{
|
||||
// See if it can be direct played
|
||||
@ -504,6 +514,10 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
if (directPlay == null)
|
||||
{
|
||||
_logger.Debug("Profile: {0}, No direct play profiles found for Path: {1}",
|
||||
profile.Name ?? "Unknown Profile",
|
||||
mediaSource.Path ?? "Unknown path");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -550,6 +564,11 @@ namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
if (!conditionProcessor.IsVideoConditionSatisfied(i, audioBitrate, audioChannels, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isCabac, refFrames, numVideoStreams, numAudioStreams))
|
||||
{
|
||||
_logger.Debug("Profile: {0}, DirectPlay=false. Reason=VideoContainerProfile.{1} Path: {2}",
|
||||
profile.Name ?? "Unknown Profile",
|
||||
i.Property,
|
||||
mediaSource.Path ?? "Unknown path");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -558,6 +577,10 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
if (string.IsNullOrEmpty(videoCodec))
|
||||
{
|
||||
_logger.Debug("Profile: {0}, DirectPlay=false. Reason=Unknown video codec. Path: {1}",
|
||||
profile.Name ?? "Unknown Profile",
|
||||
mediaSource.Path ?? "Unknown path");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -577,6 +600,11 @@ namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
if (!conditionProcessor.IsVideoConditionSatisfied(i, audioBitrate, audioChannels, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isCabac, refFrames, numVideoStreams, numAudioStreams))
|
||||
{
|
||||
_logger.Debug("Profile: {0}, DirectPlay=false. Reason=VideoCodecProfile.{1} Path: {2}",
|
||||
profile.Name ?? "Unknown Profile",
|
||||
i.Property,
|
||||
mediaSource.Path ?? "Unknown path");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -587,6 +615,10 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
if (string.IsNullOrEmpty(audioCodec))
|
||||
{
|
||||
_logger.Debug("Profile: {0}, DirectPlay=false. Reason=Unknown audio codec. Path: {1}",
|
||||
profile.Name ?? "Unknown Profile",
|
||||
mediaSource.Path ?? "Unknown path");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -607,6 +639,11 @@ namespace MediaBrowser.Model.Dlna
|
||||
bool? isSecondaryAudio = audioStream == null ? null : mediaSource.IsSecondaryAudio(audioStream);
|
||||
if (!conditionProcessor.IsVideoAudioConditionSatisfied(i, audioChannels, audioBitrate, audioProfile, isSecondaryAudio))
|
||||
{
|
||||
_logger.Debug("Profile: {0}, DirectPlay=false. Reason=VideoAudioCodecProfile.{1} Path: {2}",
|
||||
profile.Name ?? "Unknown Profile",
|
||||
i.Property,
|
||||
mediaSource.Path ?? "Unknown path");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -70,6 +70,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
public string SubtitleFormat { get; set; }
|
||||
|
||||
public string PlaySessionId { get; set; }
|
||||
public List<MediaSourceInfo> AllMediaSources { get; set; }
|
||||
|
||||
public string MediaSourceId
|
||||
{
|
||||
|
@ -1,7 +1,7 @@
|
||||
using System.ComponentModel;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.Serialization;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
|
||||
namespace MediaBrowser.Model.Dto
|
||||
{
|
||||
|
@ -1,26 +1,65 @@
|
||||
using MediaBrowser.Model.Dto;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Model.Entities
|
||||
{
|
||||
public class MediaInfo
|
||||
public class MediaInfo : MediaSourceInfo, IHasProviderIds
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the media streams.
|
||||
/// </summary>
|
||||
/// <value>The media streams.</value>
|
||||
public List<MediaStream> MediaStreams { get; set; }
|
||||
public List<ChapterInfo> Chapters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format.
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
/// <value>The format.</value>
|
||||
public string Format { get; set; }
|
||||
|
||||
public int? TotalBitrate { get; set; }
|
||||
/// <value>The title.</value>
|
||||
public string Title { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the album.
|
||||
/// </summary>
|
||||
/// <value>The album.</value>
|
||||
public string Album { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the artists.
|
||||
/// </summary>
|
||||
/// <value>The artists.</value>
|
||||
public List<string> Artists { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the album artists.
|
||||
/// </summary>
|
||||
/// <value>The album artists.</value>
|
||||
public List<string> AlbumArtists { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the studios.
|
||||
/// </summary>
|
||||
/// <value>The studios.</value>
|
||||
public List<string> Studios { get; set; }
|
||||
public List<string> Genres { get; set; }
|
||||
public int? IndexNumber { get; set; }
|
||||
public int? ParentIndexNumber { get; set; }
|
||||
public int? ProductionYear { get; set; }
|
||||
public DateTime? PremiereDate { get; set; }
|
||||
public List<BaseItemPerson> People { get; set; }
|
||||
public Dictionary<string, string> ProviderIds { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the official rating.
|
||||
/// </summary>
|
||||
/// <value>The official rating.</value>
|
||||
public string OfficialRating { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the overview.
|
||||
/// </summary>
|
||||
/// <value>The overview.</value>
|
||||
public string Overview { get; set; }
|
||||
|
||||
public MediaInfo()
|
||||
{
|
||||
MediaStreams = new List<MediaStream>();
|
||||
Chapters = new List<ChapterInfo>();
|
||||
Artists = new List<string>();
|
||||
AlbumArtists = new List<string>();
|
||||
Studios = new List<string>();
|
||||
Genres = new List<string>();
|
||||
People = new List<BaseItemPerson>();
|
||||
ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
@ -102,7 +102,6 @@
|
||||
<Compile Include="Manager\MetadataService.cs" />
|
||||
<Compile Include="Manager\SeriesOrderManager.cs" />
|
||||
<Compile Include="MediaInfo\FFProbeAudioInfo.cs" />
|
||||
<Compile Include="MediaInfo\FFProbeHelpers.cs" />
|
||||
<Compile Include="MediaInfo\FFProbeProvider.cs" />
|
||||
<Compile Include="MediaInfo\FFProbeVideoInfo.cs" />
|
||||
<Compile Include="MediaInfo\SubtitleDownloader.cs" />
|
||||
@ -198,10 +197,6 @@
|
||||
<Project>{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}</Project>
|
||||
<Name>MediaBrowser.Controller</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\MediaBrowser.MediaInfo\MediaBrowser.MediaInfo.csproj">
|
||||
<Project>{6e4145e4-c6d4-4e4d-94f2-87188db6e239}</Project>
|
||||
<Name>MediaBrowser.MediaInfo</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
|
||||
<Project>{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}</Project>
|
||||
<Name>MediaBrowser.Model</Name>
|
||||
|
@ -1,5 +1,4 @@
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@ -8,8 +7,6 @@ using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -49,18 +46,14 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
FFProbeHelpers.NormalizeFFProbeResult(result);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await Fetch(item, cancellationToken, result).ConfigureAwait(false);
|
||||
|
||||
return ItemUpdateType.MetadataImport;
|
||||
}
|
||||
|
||||
private const string SchemaVersion = "1";
|
||||
private const string SchemaVersion = "2";
|
||||
|
||||
private async Task<InternalMediaInfoResult> GetMediaInfo(BaseItem item, CancellationToken cancellationToken)
|
||||
private async Task<Model.Entities.MediaInfo> GetMediaInfo(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
@ -71,7 +64,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
try
|
||||
{
|
||||
return _json.DeserializeFromFile<InternalMediaInfoResult>(cachePath);
|
||||
return _json.DeserializeFromFile<Model.Entities.MediaInfo>(cachePath);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
@ -83,7 +76,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
var inputPath = new[] { item.Path };
|
||||
|
||||
var result = await _mediaEncoder.GetMediaInfo(inputPath, MediaProtocol.File, false, cancellationToken).ConfigureAwait(false);
|
||||
var result = await _mediaEncoder.GetMediaInfo(inputPath, item.Path, MediaProtocol.File, true, false, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cachePath));
|
||||
_json.SerializeToFile(result, cachePath);
|
||||
@ -96,61 +89,23 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
/// </summary>
|
||||
/// <param name="audio">The audio.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="mediaInfo">The media information.</param>
|
||||
/// <returns>Task.</returns>
|
||||
protected Task Fetch(Audio audio, CancellationToken cancellationToken, InternalMediaInfoResult data)
|
||||
protected Task Fetch(Audio audio, CancellationToken cancellationToken, Model.Entities.MediaInfo mediaInfo)
|
||||
{
|
||||
var mediaInfo = MediaEncoderHelpers.GetMediaInfo(data);
|
||||
var mediaStreams = mediaInfo.MediaStreams;
|
||||
|
||||
audio.FormatName = mediaInfo.Format;
|
||||
audio.TotalBitrate = mediaInfo.TotalBitrate;
|
||||
audio.FormatName = mediaInfo.Container;
|
||||
audio.TotalBitrate = mediaInfo.Bitrate;
|
||||
audio.HasEmbeddedImage = mediaStreams.Any(i => i.Type == MediaStreamType.EmbeddedImage);
|
||||
|
||||
if (data.streams != null)
|
||||
{
|
||||
// Get the first audio stream
|
||||
var stream = data.streams.FirstOrDefault(s => string.Equals(s.codec_type, "audio", StringComparison.OrdinalIgnoreCase));
|
||||
audio.RunTimeTicks = mediaInfo.RunTimeTicks;
|
||||
audio.Size = mediaInfo.Size;
|
||||
|
||||
if (stream != null)
|
||||
{
|
||||
// Get duration from stream properties
|
||||
var duration = stream.duration;
|
||||
var extension = (Path.GetExtension(audio.Path) ?? string.Empty).TrimStart('.');
|
||||
audio.Container = extension;
|
||||
|
||||
// If it's not there go into format properties
|
||||
if (string.IsNullOrEmpty(duration))
|
||||
{
|
||||
duration = data.format.duration;
|
||||
}
|
||||
|
||||
// If we got something, parse it
|
||||
if (!string.IsNullOrEmpty(duration))
|
||||
{
|
||||
audio.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration, _usCulture)).Ticks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.format != null)
|
||||
{
|
||||
var extension = (Path.GetExtension(audio.Path) ?? string.Empty).TrimStart('.');
|
||||
|
||||
audio.Container = extension;
|
||||
|
||||
if (!string.IsNullOrEmpty(data.format.size))
|
||||
{
|
||||
audio.Size = long.Parse(data.format.size, _usCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
audio.Size = null;
|
||||
}
|
||||
|
||||
if (data.format.tags != null)
|
||||
{
|
||||
FetchDataFromTags(audio, data.format.tags);
|
||||
}
|
||||
}
|
||||
FetchDataFromTags(audio, mediaInfo);
|
||||
|
||||
return _itemRepo.SaveMediaStreams(audio.Id, mediaStreams, cancellationToken);
|
||||
}
|
||||
@ -159,92 +114,36 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
/// Fetches data from the tags dictionary
|
||||
/// </summary>
|
||||
/// <param name="audio">The audio.</param>
|
||||
/// <param name="tags">The tags.</param>
|
||||
private void FetchDataFromTags(Audio audio, Dictionary<string, string> tags)
|
||||
/// <param name="data">The data.</param>
|
||||
private void FetchDataFromTags(Audio audio, Model.Entities.MediaInfo data)
|
||||
{
|
||||
var title = FFProbeHelpers.GetDictionaryValue(tags, "title");
|
||||
|
||||
// Only set Name if title was found in the dictionary
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
if (!string.IsNullOrEmpty(data.Title))
|
||||
{
|
||||
audio.Name = title;
|
||||
audio.Name = data.Title;
|
||||
}
|
||||
|
||||
if (!audio.LockedFields.Contains(MetadataFields.Cast))
|
||||
{
|
||||
audio.People.Clear();
|
||||
|
||||
var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(composer))
|
||||
foreach (var person in data.People)
|
||||
{
|
||||
foreach (var person in Split(composer, false))
|
||||
audio.AddPerson(new PersonInfo
|
||||
{
|
||||
audio.AddPerson(new PersonInfo { Name = person, Type = PersonType.Composer });
|
||||
}
|
||||
Name = person.Name,
|
||||
Type = person.Type,
|
||||
Role = person.Role
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
audio.Album = FFProbeHelpers.GetDictionaryValue(tags, "album");
|
||||
|
||||
var artists = FFProbeHelpers.GetDictionaryValue(tags, "artists");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(artists))
|
||||
{
|
||||
audio.Artists = artists.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist");
|
||||
if (string.IsNullOrWhiteSpace(artist))
|
||||
{
|
||||
audio.Artists.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
audio.Artists = SplitArtists(artist)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
var albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "albumartist");
|
||||
if (string.IsNullOrWhiteSpace(albumArtist))
|
||||
{
|
||||
albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "album artist");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(albumArtist))
|
||||
{
|
||||
albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "album_artist");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(albumArtist))
|
||||
{
|
||||
audio.AlbumArtists = new List<string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
audio.AlbumArtists = SplitArtists(albumArtist)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
}
|
||||
|
||||
// Track number
|
||||
audio.IndexNumber = GetDictionaryDiscValue(tags, "track");
|
||||
|
||||
// Disc number
|
||||
audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc");
|
||||
|
||||
audio.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date");
|
||||
|
||||
// Several different forms of retaildate
|
||||
audio.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ??
|
||||
FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ??
|
||||
FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ??
|
||||
FFProbeHelpers.GetDictionaryDateTime(tags, "date");
|
||||
audio.Album = data.Album;
|
||||
audio.Artists = data.Artists;
|
||||
audio.AlbumArtists = data.AlbumArtists;
|
||||
audio.IndexNumber = data.IndexNumber;
|
||||
audio.ProductionYear = data.ProductionYear;
|
||||
audio.PremiereDate = data.PremiereDate;
|
||||
|
||||
// If we don't have a ProductionYear try and get it from PremiereDate
|
||||
if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue)
|
||||
@ -254,192 +153,29 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
if (!audio.LockedFields.Contains(MetadataFields.Genres))
|
||||
{
|
||||
FetchGenres(audio, tags);
|
||||
audio.Genres.Clear();
|
||||
|
||||
foreach (var genre in data.Genres)
|
||||
{
|
||||
audio.AddGenre(genre);
|
||||
}
|
||||
}
|
||||
|
||||
if (!audio.LockedFields.Contains(MetadataFields.Studios))
|
||||
{
|
||||
audio.Studios.Clear();
|
||||
|
||||
// There's several values in tags may or may not be present
|
||||
FetchStudios(audio, tags, "organization");
|
||||
FetchStudios(audio, tags, "ensemble");
|
||||
FetchStudios(audio, tags, "publisher");
|
||||
}
|
||||
|
||||
// These support mulitple values, but for now we only store the first.
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzAlbumArtist, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Artist Id")));
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzArtist, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Artist Id")));
|
||||
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzAlbum, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Id")));
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzReleaseGroup, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Group Id")));
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzTrack, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Track Id")));
|
||||
}
|
||||
|
||||
private string GetMultipleMusicBrainzId(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(i => i.Trim())
|
||||
.FirstOrDefault(i => !string.IsNullOrWhiteSpace(i));
|
||||
}
|
||||
|
||||
private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
|
||||
|
||||
/// <summary>
|
||||
/// Splits the specified val.
|
||||
/// </summary>
|
||||
/// <param name="val">The val.</param>
|
||||
/// <param name="allowCommaDelimiter">if set to <c>true</c> [allow comma delimiter].</param>
|
||||
/// <returns>System.String[][].</returns>
|
||||
private IEnumerable<string> Split(string val, bool allowCommaDelimiter)
|
||||
{
|
||||
// Only use the comma as a delimeter if there are no slashes or pipes.
|
||||
// We want to be careful not to split names that have commas in them
|
||||
var delimeter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.IndexOf(i) != -1) ?
|
||||
_nameDelimiters :
|
||||
new[] { ',' };
|
||||
|
||||
return val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => i.Trim());
|
||||
}
|
||||
|
||||
private const string ArtistReplaceValue = " | ";
|
||||
|
||||
private IEnumerable<string> SplitArtists(string val)
|
||||
{
|
||||
val = val.Replace(" featuring ", ArtistReplaceValue, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(" feat. ", ArtistReplaceValue, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var artistsFound = new List<string>();
|
||||
|
||||
foreach (var whitelistArtist in GetSplitWhitelist())
|
||||
{
|
||||
var originalVal = val;
|
||||
val = val.Replace(whitelistArtist, "|", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!string.Equals(originalVal, val, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
artistsFound.Add(whitelistArtist);
|
||||
}
|
||||
}
|
||||
|
||||
// Only use the comma as a delimeter if there are no slashes or pipes.
|
||||
// We want to be careful not to split names that have commas in them
|
||||
var delimeter = _nameDelimiters;
|
||||
|
||||
var artists = val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => i.Trim());
|
||||
|
||||
artistsFound.AddRange(artists);
|
||||
return artistsFound;
|
||||
}
|
||||
|
||||
|
||||
private List<string> _splitWhiteList = null;
|
||||
|
||||
private IEnumerable<string> GetSplitWhitelist()
|
||||
{
|
||||
if (_splitWhiteList == null)
|
||||
{
|
||||
var file = GetType().Namespace + ".whitelist.txt";
|
||||
|
||||
using (var stream = GetType().Assembly.GetManifestResourceStream(file))
|
||||
{
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
var list = new List<string>();
|
||||
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
var val = reader.ReadLine();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
list.Add(val);
|
||||
}
|
||||
}
|
||||
|
||||
_splitWhiteList = list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _splitWhiteList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the studios from the tags collection
|
||||
/// </summary>
|
||||
/// <param name="audio">The audio.</param>
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="tagName">Name of the tag.</param>
|
||||
private void FetchStudios(Audio audio, Dictionary<string, string> tags, string tagName)
|
||||
{
|
||||
var val = FFProbeHelpers.GetDictionaryValue(tags, tagName);
|
||||
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
// Sometimes the artist name is listed here, account for that
|
||||
var studios = Split(val, true).Where(i => !audio.HasAnyArtist(i));
|
||||
|
||||
foreach (var studio in studios)
|
||||
foreach (var studio in data.Studios)
|
||||
{
|
||||
audio.AddStudio(studio);
|
||||
}
|
||||
}
|
||||
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzAlbumArtist, data.GetProviderId(MetadataProviders.MusicBrainzAlbumArtist));
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzArtist, data.GetProviderId(MetadataProviders.MusicBrainzArtist));
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzAlbum, data.GetProviderId(MetadataProviders.MusicBrainzAlbum));
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzReleaseGroup, data.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup));
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzTrack, data.GetProviderId(MetadataProviders.MusicBrainzTrack));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the genres from the tags collection
|
||||
/// </summary>
|
||||
/// <param name="audio">The audio.</param>
|
||||
/// <param name="tags">The tags.</param>
|
||||
private void FetchGenres(Audio audio, Dictionary<string, string> tags)
|
||||
{
|
||||
var val = FFProbeHelpers.GetDictionaryValue(tags, "genre");
|
||||
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
audio.Genres.Clear();
|
||||
|
||||
foreach (var genre in Split(val, true))
|
||||
{
|
||||
audio.AddGenre(genre);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the disc number, which is sometimes can be in the form of '1', or '1/3'
|
||||
/// </summary>
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="tagName">Name of the tag.</param>
|
||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||
private int? GetDictionaryDiscValue(Dictionary<string, string> tags, string tagName)
|
||||
{
|
||||
var disc = FFProbeHelpers.GetDictionaryValue(tags, tagName);
|
||||
|
||||
if (!string.IsNullOrEmpty(disc))
|
||||
{
|
||||
disc = disc.Split('/')[0];
|
||||
|
||||
int num;
|
||||
|
||||
if (int.TryParse(disc, out num))
|
||||
{
|
||||
return num;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Subtitles;
|
||||
using MediaBrowser.MediaInfo;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
@ -116,10 +115,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
FFProbeHelpers.NormalizeFFProbeResult(result);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await Fetch(item, cancellationToken, result, isoMount, blurayDiscInfo, options).ConfigureAwait(false);
|
||||
|
||||
}
|
||||
@ -136,7 +131,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
private const string SchemaVersion = "1";
|
||||
|
||||
private async Task<InternalMediaInfoResult> GetMediaInfo(Video item,
|
||||
private async Task<Model.Entities.MediaInfo> GetMediaInfo(Video item,
|
||||
IIsoMount isoMount,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@ -149,7 +144,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
try
|
||||
{
|
||||
return _json.DeserializeFromFile<InternalMediaInfoResult>(cachePath);
|
||||
return _json.DeserializeFromFile<Model.Entities.MediaInfo>(cachePath);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
@ -165,7 +160,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
var inputPath = MediaEncoderHelpers.GetInputArgument(item.Path, protocol, isoMount, item.PlayableStreamFileNames);
|
||||
|
||||
var result = await _mediaEncoder.GetMediaInfo(inputPath, protocol, false, cancellationToken).ConfigureAwait(false);
|
||||
var result = await _mediaEncoder.GetMediaInfo(inputPath, item.Path, protocol, false, true, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cachePath));
|
||||
_json.SerializeToFile(result, cachePath);
|
||||
@ -175,52 +170,37 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
protected async Task Fetch(Video video,
|
||||
CancellationToken cancellationToken,
|
||||
InternalMediaInfoResult data,
|
||||
Model.Entities.MediaInfo mediaInfo,
|
||||
IIsoMount isoMount,
|
||||
BlurayDiscInfo blurayInfo,
|
||||
MetadataRefreshOptions options)
|
||||
{
|
||||
var mediaInfo = MediaEncoderHelpers.GetMediaInfo(data);
|
||||
var mediaStreams = mediaInfo.MediaStreams;
|
||||
|
||||
video.TotalBitrate = mediaInfo.TotalBitrate;
|
||||
video.FormatName = (mediaInfo.Format ?? string.Empty)
|
||||
video.TotalBitrate = mediaInfo.Bitrate;
|
||||
video.FormatName = (mediaInfo.Container ?? string.Empty)
|
||||
.Replace("matroska", "mkv", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (data.format != null)
|
||||
// For dvd's this may not always be accurate, so don't set the runtime if the item already has one
|
||||
var needToSetRuntime = video.VideoType != VideoType.Dvd || video.RunTimeTicks == null || video.RunTimeTicks.Value == 0;
|
||||
|
||||
if (needToSetRuntime)
|
||||
{
|
||||
// For dvd's this may not always be accurate, so don't set the runtime if the item already has one
|
||||
var needToSetRuntime = video.VideoType != VideoType.Dvd || video.RunTimeTicks == null || video.RunTimeTicks.Value == 0;
|
||||
|
||||
if (needToSetRuntime && !string.IsNullOrEmpty(data.format.duration))
|
||||
{
|
||||
video.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration, _usCulture)).Ticks;
|
||||
}
|
||||
|
||||
if (video.VideoType == VideoType.VideoFile)
|
||||
{
|
||||
var extension = (Path.GetExtension(video.Path) ?? string.Empty).TrimStart('.');
|
||||
|
||||
video.Container = extension;
|
||||
}
|
||||
else
|
||||
{
|
||||
video.Container = null;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(data.format.size))
|
||||
{
|
||||
video.Size = long.Parse(data.format.size, _usCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
video.Size = null;
|
||||
}
|
||||
video.RunTimeTicks = mediaInfo.RunTimeTicks;
|
||||
}
|
||||
|
||||
var mediaChapters = (data.Chapters ?? new MediaChapter[] { }).ToList();
|
||||
var chapters = mediaChapters.Select(GetChapterInfo).ToList();
|
||||
if (video.VideoType == VideoType.VideoFile)
|
||||
{
|
||||
var extension = (Path.GetExtension(video.Path) ?? string.Empty).TrimStart('.');
|
||||
|
||||
video.Container = extension;
|
||||
}
|
||||
else
|
||||
{
|
||||
video.Container = null;
|
||||
}
|
||||
|
||||
var chapters = mediaInfo.Chapters ?? new List<ChapterInfo>();
|
||||
if (video.VideoType == VideoType.BluRay || (video.IsoType.HasValue && video.IsoType.Value == IsoType.BluRay))
|
||||
{
|
||||
FetchBdInfo(video, chapters, mediaStreams, blurayInfo);
|
||||
@ -228,7 +208,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
FetchWtvInfo(video, data);
|
||||
FetchEmbeddedInfo(video, mediaInfo);
|
||||
|
||||
video.IsHD = mediaStreams.Any(i => i.Type == MediaStreamType.Video && i.Width.HasValue && i.Width.Value >= 1270);
|
||||
|
||||
@ -238,9 +218,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
video.DefaultVideoStreamIndex = videoStream == null ? (int?)null : videoStream.Index;
|
||||
|
||||
video.HasSubtitles = mediaStreams.Any(i => i.Type == MediaStreamType.Subtitle);
|
||||
|
||||
ExtractTimestamp(video);
|
||||
UpdateFromMediaInfo(video, videoStream);
|
||||
video.Timestamp = mediaInfo.Timestamp;
|
||||
|
||||
await _itemRepo.SaveMediaStreams(video.Id, mediaStreams, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@ -283,29 +261,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateFromMediaInfo(Video video, MediaStream videoStream)
|
||||
{
|
||||
if (video.VideoType == VideoType.VideoFile && video.LocationType != LocationType.Remote && video.LocationType != LocationType.Virtual)
|
||||
{
|
||||
if (videoStream != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = new MediaInfoLib().GetVideoInfo(video.Path);
|
||||
|
||||
videoStream.IsCabac = result.IsCabac ?? videoStream.IsCabac;
|
||||
videoStream.IsInterlaced = result.IsInterlaced ?? videoStream.IsInterlaced;
|
||||
videoStream.BitDepth = result.BitDepth ?? videoStream.BitDepth;
|
||||
videoStream.RefFrames = result.RefFrames;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error running MediaInfo on {0}", ex, video.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void NormalizeChapterNames(List<ChapterInfo> chapters)
|
||||
{
|
||||
var index = 1;
|
||||
@ -325,32 +280,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
}
|
||||
}
|
||||
|
||||
private ChapterInfo GetChapterInfo(MediaChapter chapter)
|
||||
{
|
||||
var info = new ChapterInfo();
|
||||
|
||||
if (chapter.tags != null)
|
||||
{
|
||||
string name;
|
||||
if (chapter.tags.TryGetValue("title", out name))
|
||||
{
|
||||
info.Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
// Limit accuracy to milliseconds to match xml saving
|
||||
var secondsString = chapter.start_time;
|
||||
double seconds;
|
||||
|
||||
if (double.TryParse(secondsString, NumberStyles.Any, CultureInfo.InvariantCulture, out seconds))
|
||||
{
|
||||
var ms = Math.Round(TimeSpan.FromSeconds(seconds).TotalMilliseconds);
|
||||
info.StartPositionTicks = TimeSpan.FromMilliseconds(ms).Ticks;
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private void FetchBdInfo(BaseItem item, List<ChapterInfo> chapters, List<MediaStream> mediaStreams, BlurayDiscInfo blurayInfo)
|
||||
{
|
||||
var video = (Video)item;
|
||||
@ -419,129 +348,79 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
return _blurayExaminer.GetDiscInfo(path);
|
||||
}
|
||||
|
||||
public const int MaxSubtitleDescriptionExtractionLength = 100; // When extracting subtitles, the maximum length to consider (to avoid invalid filenames)
|
||||
|
||||
private void FetchWtvInfo(Video video, InternalMediaInfoResult data)
|
||||
private void FetchEmbeddedInfo(Video video, Model.Entities.MediaInfo data)
|
||||
{
|
||||
if (data.format == null || data.format.tags == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!video.LockedFields.Contains(MetadataFields.Genres))
|
||||
{
|
||||
var genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/Genre");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(genres))
|
||||
{
|
||||
//genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "genre");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(genres))
|
||||
{
|
||||
video.Genres = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => i.Trim())
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
if (!video.LockedFields.Contains(MetadataFields.OfficialRating))
|
||||
{
|
||||
var officialRating = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/ParentalRating");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(officialRating))
|
||||
if (!string.IsNullOrWhiteSpace(data.OfficialRating))
|
||||
{
|
||||
video.OfficialRating = officialRating;
|
||||
video.OfficialRating = data.OfficialRating;
|
||||
}
|
||||
}
|
||||
|
||||
if (!video.LockedFields.Contains(MetadataFields.Cast))
|
||||
{
|
||||
var people = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaCredits");
|
||||
video.People.Clear();
|
||||
|
||||
if (!string.IsNullOrEmpty(people))
|
||||
foreach (var person in data.People)
|
||||
{
|
||||
video.People = people.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => new PersonInfo { Name = i.Trim(), Type = PersonType.Actor })
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
var year = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/OriginalReleaseTime");
|
||||
if (!string.IsNullOrWhiteSpace(year))
|
||||
{
|
||||
int val;
|
||||
|
||||
if (int.TryParse(year, NumberStyles.Integer, _usCulture, out val))
|
||||
{
|
||||
video.ProductionYear = val;
|
||||
}
|
||||
}
|
||||
|
||||
var premiereDateString = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaOriginalBroadcastDateTime");
|
||||
if (!string.IsNullOrWhiteSpace(premiereDateString))
|
||||
{
|
||||
DateTime val;
|
||||
|
||||
// Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
|
||||
// DateTime is reported along with timezone info (typically Z i.e. UTC hence assume None)
|
||||
if (DateTime.TryParse(year, null, DateTimeStyles.None, out val))
|
||||
{
|
||||
video.PremiereDate = val.ToUniversalTime();
|
||||
}
|
||||
}
|
||||
|
||||
var description = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitleDescription");
|
||||
|
||||
var episode = video as Episode;
|
||||
if (episode != null)
|
||||
{
|
||||
var subTitle = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitle");
|
||||
|
||||
// For below code, credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
|
||||
|
||||
// Sometimes for TV Shows the Subtitle field is empty and the subtitle description contains the subtitle, extract if possible. See ticket https://mcebuddy2x.codeplex.com/workitem/1910
|
||||
// The format is -> EPISODE/TOTAL_EPISODES_IN_SEASON. SUBTITLE: DESCRIPTION
|
||||
// OR -> COMMENT. SUBTITLE: DESCRIPTION
|
||||
// e.g. -> 4/13. The Doctor's Wife: Science fiction drama. When he follows a Time Lord distress signal, the Doctor puts Amy, Rory and his beloved TARDIS in grave danger. Also in HD. [AD,S]
|
||||
// e.g. -> CBeebies Bedtime Hour. The Mystery: Animated adventures of two friends who live on an island in the middle of the big city. Some of Abney and Teal's favourite objects are missing. [S]
|
||||
if (String.IsNullOrWhiteSpace(subTitle) && !String.IsNullOrWhiteSpace(description) && description.Substring(0, Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)).Contains(":")) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
|
||||
{
|
||||
string[] parts = description.Split(':');
|
||||
if (parts.Length > 0)
|
||||
video.AddPerson(new PersonInfo
|
||||
{
|
||||
string subtitle = parts[0];
|
||||
try
|
||||
{
|
||||
if (subtitle.Contains("/")) // It contains a episode number and season number
|
||||
{
|
||||
string[] numbers = subtitle.Split(' ');
|
||||
episode.IndexNumber = int.Parse(numbers[0].Replace(".", "").Split('/')[0]);
|
||||
int totalEpisodesInSeason = int.Parse(numbers[0].Replace(".", "").Split('/')[1]);
|
||||
|
||||
description = String.Join(" ", numbers, 1, numbers.Length - 1).Trim(); // Skip the first, concatenate the rest, clean up spaces and save it
|
||||
}
|
||||
else
|
||||
throw new Exception(); // Switch to default parsing
|
||||
}
|
||||
catch // Default parsing
|
||||
{
|
||||
if (subtitle.Contains(".")) // skip the comment, keep the subtitle
|
||||
description = String.Join(".", subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first
|
||||
else
|
||||
description = subtitle.Trim(); // Clean up whitespaces and save it
|
||||
}
|
||||
}
|
||||
Name = person.Name,
|
||||
Type = person.Type,
|
||||
Role = person.Role
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!video.LockedFields.Contains(MetadataFields.Genres))
|
||||
{
|
||||
video.Genres.Clear();
|
||||
|
||||
foreach (var genre in data.Genres)
|
||||
{
|
||||
video.AddGenre(genre);
|
||||
}
|
||||
}
|
||||
|
||||
if (!video.LockedFields.Contains(MetadataFields.Studios))
|
||||
{
|
||||
video.Studios.Clear();
|
||||
|
||||
foreach (var studio in data.Studios)
|
||||
{
|
||||
video.AddStudio(studio);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.ProductionYear.HasValue)
|
||||
{
|
||||
video.ProductionYear = data.ProductionYear;
|
||||
}
|
||||
if (data.PremiereDate.HasValue)
|
||||
{
|
||||
video.PremiereDate = data.PremiereDate;
|
||||
}
|
||||
if (data.IndexNumber.HasValue)
|
||||
{
|
||||
video.IndexNumber = data.IndexNumber;
|
||||
}
|
||||
if (data.ParentIndexNumber.HasValue)
|
||||
{
|
||||
video.ParentIndexNumber = data.ParentIndexNumber;
|
||||
}
|
||||
|
||||
// If we don't have a ProductionYear try and get it from PremiereDate
|
||||
if (video.PremiereDate.HasValue && !video.ProductionYear.HasValue)
|
||||
{
|
||||
video.ProductionYear = video.PremiereDate.Value.ToLocalTime().Year;
|
||||
}
|
||||
|
||||
if (!video.LockedFields.Contains(MetadataFields.Overview))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(description))
|
||||
if (!string.IsNullOrWhiteSpace(data.Overview))
|
||||
{
|
||||
video.Overview = description;
|
||||
video.Overview = data.Overview;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -709,56 +588,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtractTimestamp(Video video)
|
||||
{
|
||||
if (video.VideoType == VideoType.VideoFile)
|
||||
{
|
||||
if (string.Equals(video.Container, "mpeg2ts", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(video.Container, "m2ts", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(video.Container, "ts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
video.Timestamp = GetMpegTimestamp(video.Path);
|
||||
|
||||
_logger.Debug("Video has {0} timestamp", video.Timestamp);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error extracting timestamp info from {0}", ex, video.Path);
|
||||
video.Timestamp = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TransportStreamTimestamp GetMpegTimestamp(string path)
|
||||
{
|
||||
var packetBuffer = new byte['Å'];
|
||||
|
||||
using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
fs.Read(packetBuffer, 0, packetBuffer.Length);
|
||||
}
|
||||
|
||||
if (packetBuffer[0] == 71)
|
||||
{
|
||||
return TransportStreamTimestamp.None;
|
||||
}
|
||||
|
||||
if ((packetBuffer[4] == 71) && (packetBuffer['Ä'] == 71))
|
||||
{
|
||||
if ((packetBuffer[0] == 0) && (packetBuffer[1] == 0) && (packetBuffer[2] == 0) && (packetBuffer[3] == 0))
|
||||
{
|
||||
return TransportStreamTimestamp.Zero;
|
||||
}
|
||||
|
||||
return TransportStreamTimestamp.Valid;
|
||||
}
|
||||
|
||||
return TransportStreamTimestamp.None;
|
||||
}
|
||||
|
||||
private void FetchFromDvdLib(Video item, IIsoMount mount)
|
||||
{
|
||||
var path = mount == null ? item.Path : mount.MountedPath;
|
||||
|
@ -1,6 +1,7 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
@ -18,12 +19,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
|
||||
public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IJsonSerializer jsonSerializer, ILogManager logManager, IMediaSourceManager mediaSourceManager)
|
||||
public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IJsonSerializer jsonSerializer, ILogManager logManager, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
|
||||
{
|
||||
_liveTvManager = liveTvManager;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_logger = logManager.GetLogger(GetType().Name);
|
||||
}
|
||||
|
||||
@ -90,14 +93,86 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
|
||||
public async Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken)
|
||||
{
|
||||
MediaSourceInfo stream;
|
||||
var isAudio = false;
|
||||
|
||||
var keys = openToken.Split(new[] { '|' }, 2);
|
||||
|
||||
if (string.Equals(keys[0], typeof(LiveTvChannel).Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return await _liveTvManager.GetChannelStream(keys[1], cancellationToken).ConfigureAwait(false);
|
||||
stream = await _liveTvManager.GetChannelStream(keys[1], cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
stream = await _liveTvManager.GetRecordingStream(keys[1], cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return await _liveTvManager.GetRecordingStream(keys[1], cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await AddMediaInfo(stream, isAudio, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error probing live tv stream", ex);
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)
|
||||
{
|
||||
var inputPaths = new[] { mediaSource.Path };
|
||||
|
||||
var info = await _mediaEncoder.GetMediaInfo(inputPaths, mediaSource.Path, mediaSource.Protocol, isAudio, false, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
mediaSource.Bitrate = info.Bitrate;
|
||||
mediaSource.Container = info.Container;
|
||||
mediaSource.Formats = info.Formats;
|
||||
mediaSource.MediaStreams = info.MediaStreams;
|
||||
mediaSource.RunTimeTicks = info.RunTimeTicks;
|
||||
mediaSource.Size = info.Size;
|
||||
mediaSource.Timestamp = info.Timestamp;
|
||||
mediaSource.Video3DFormat = info.Video3DFormat;
|
||||
mediaSource.VideoType = info.VideoType;
|
||||
|
||||
mediaSource.DefaultSubtitleStreamIndex = null;
|
||||
|
||||
var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Audio);
|
||||
|
||||
if (audioStream == null || audioStream.Index == -1)
|
||||
{
|
||||
mediaSource.DefaultAudioStreamIndex = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
mediaSource.DefaultAudioStreamIndex = audioStream.Index;
|
||||
}
|
||||
|
||||
// Try to estimate this
|
||||
if (!mediaSource.Bitrate.HasValue)
|
||||
{
|
||||
var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Video);
|
||||
if (videoStream != null)
|
||||
{
|
||||
var width = videoStream.Width ?? 1920;
|
||||
|
||||
if (width >= 1900)
|
||||
{
|
||||
mediaSource.Bitrate = 10000000;
|
||||
}
|
||||
|
||||
else if (width >= 1260)
|
||||
{
|
||||
mediaSource.Bitrate = 6000000;
|
||||
}
|
||||
|
||||
else if (width >= 700)
|
||||
{
|
||||
mediaSource.Bitrate = 4000000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task CloseMediaSource(string liveStreamId, CancellationToken cancellationToken)
|
||||
|
@ -493,7 +493,7 @@ namespace MediaBrowser.Server.Implementations.Sync
|
||||
conversionOptions.ItemId = item.Id.ToString("N");
|
||||
conversionOptions.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, false, user).ToList();
|
||||
|
||||
var streamInfo = new StreamBuilder().BuildVideoItem(conversionOptions);
|
||||
var streamInfo = new StreamBuilder(_logger).BuildVideoItem(conversionOptions);
|
||||
var mediaSource = streamInfo.MediaSource;
|
||||
|
||||
// No sense creating external subs if we're already burning one into the video
|
||||
@ -690,7 +690,7 @@ namespace MediaBrowser.Server.Implementations.Sync
|
||||
conversionOptions.ItemId = item.Id.ToString("N");
|
||||
conversionOptions.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, false, user).ToList();
|
||||
|
||||
var streamInfo = new StreamBuilder().BuildAudioItem(conversionOptions);
|
||||
var streamInfo = new StreamBuilder(_logger).BuildAudioItem(conversionOptions);
|
||||
var mediaSource = streamInfo.MediaSource;
|
||||
|
||||
jobItem.MediaSourceId = streamInfo.MediaSourceId;
|
||||
|
@ -2,7 +2,7 @@
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>MediaBrowser.Common.Internal</id>
|
||||
<version>3.0.613</version>
|
||||
<version>3.0.615</version>
|
||||
<title>MediaBrowser.Common.Internal</title>
|
||||
<authors>Luke</authors>
|
||||
<owners>ebr,Luke,scottisafool</owners>
|
||||
@ -12,7 +12,7 @@
|
||||
<description>Contains common components shared by Emby Theater and Emby Server. Not intended for plugin developer consumption.</description>
|
||||
<copyright>Copyright © Emby 2013</copyright>
|
||||
<dependencies>
|
||||
<dependency id="MediaBrowser.Common" version="3.0.613" />
|
||||
<dependency id="MediaBrowser.Common" version="3.0.615" />
|
||||
<dependency id="NLog" version="3.2.0.0" />
|
||||
<dependency id="SimpleInjector" version="2.7.0" />
|
||||
</dependencies>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>MediaBrowser.Common</id>
|
||||
<version>3.0.613</version>
|
||||
<version>3.0.615</version>
|
||||
<title>MediaBrowser.Common</title>
|
||||
<authors>Emby Team</authors>
|
||||
<owners>ebr,Luke,scottisafool</owners>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>MediaBrowser.Model.Signed</id>
|
||||
<version>3.0.613</version>
|
||||
<version>3.0.615</version>
|
||||
<title>MediaBrowser.Model - Signed Edition</title>
|
||||
<authors>Emby Team</authors>
|
||||
<owners>ebr,Luke,scottisafool</owners>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>MediaBrowser.Server.Core</id>
|
||||
<version>3.0.613</version>
|
||||
<version>3.0.615</version>
|
||||
<title>Media Browser.Server.Core</title>
|
||||
<authors>Emby Team</authors>
|
||||
<owners>ebr,Luke,scottisafool</owners>
|
||||
@ -12,7 +12,7 @@
|
||||
<description>Contains core components required to build plugins for Emby Server.</description>
|
||||
<copyright>Copyright © Emby 2013</copyright>
|
||||
<dependencies>
|
||||
<dependency id="MediaBrowser.Common" version="3.0.613" />
|
||||
<dependency id="MediaBrowser.Common" version="3.0.615" />
|
||||
</dependencies>
|
||||
</metadata>
|
||||
<files>
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
|
||||
//[assembly: AssemblyVersion("3.0.*")]
|
||||
[assembly: AssemblyVersion("3.0.5572.0")]
|
||||
[assembly: AssemblyVersion("3.0.*")]
|
||||
//[assembly: AssemblyVersion("3.0.5572.0")]
|
||||
|
Loading…
Reference in New Issue
Block a user