1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-26 22:46:53 -04:00

New: MediaInfo -> FFProbe

* New: MediaInfo -> FFProbe

* Detect HDR format

* Fix migration for users that tested early ffmpeg
This commit is contained in:
ta264
2021-11-09 06:31:05 +00:00
committed by GitHub
parent e7ff13085e
commit f0f8a4ffaf
30 changed files with 1455 additions and 1638 deletions
@@ -0,0 +1,903 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using Dapper;
using FluentMigrator;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Core.MediaFiles.MediaInfo;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(199)]
public class mediainfo_to_ffmpeg : NzbDroneMigrationBase
{
private readonly JsonSerializerOptions _serializerSettings;
public mediainfo_to_ffmpeg()
{
var serializerSettings = new JsonSerializerOptions
{
AllowTrailingCommas = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNameCaseInsensitive = true,
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
serializerSettings.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true));
serializerSettings.Converters.Add(new STJTimeSpanConverter());
serializerSettings.Converters.Add(new STJUtcConverter());
_serializerSettings = serializerSettings;
}
protected override void MainDbUpgrade()
{
Execute.WithConnection(MigrateToFfprobe);
}
private void MigrateToFfprobe(IDbConnection conn, IDbTransaction tran)
{
var existing = conn.Query<MediaInfoRaw>("SELECT Id, MediaInfo, SceneName FROM MovieFiles");
var updated = new List<MediaInfoRaw>();
foreach (var row in existing)
{
if (row.MediaInfo.IsNullOrWhiteSpace())
{
continue;
}
// basic parse to check schema revision
// in case user already tested ffmpeg branch
var mediaInfoVersion = JsonSerializer.Deserialize<MediaInfoBase>(row.MediaInfo, _serializerSettings);
if (mediaInfoVersion.SchemaRevision >= 8)
{
continue;
}
// parse and migrate
var mediaInfo = JsonSerializer.Deserialize<MediaInfo198>(row.MediaInfo, _serializerSettings);
var ffprobe = MigrateMediaInfo(mediaInfo, row.SceneName);
updated.Add(new MediaInfoRaw
{
Id = row.Id,
MediaInfo = JsonSerializer.Serialize(ffprobe, _serializerSettings)
});
}
var updateSql = "UPDATE MovieFiles SET MediaInfo = @MediaInfo WHERE Id = @Id";
conn.Execute(updateSql, updated, transaction: tran);
}
public MediaInfo199 MigrateMediaInfo(MediaInfo198 old, string sceneName)
{
var m = new MediaInfo199
{
SchemaRevision = old.SchemaRevision,
ContainerFormat = old.ContainerFormat,
VideoProfile = old.VideoProfile,
VideoBitrate = old.VideoBitrate,
VideoBitDepth = old.VideoBitDepth,
VideoMultiViewCount = old.VideoMultiViewCount,
VideoColourPrimaries = MigratePrimaries(old.VideoColourPrimaries),
VideoTransferCharacteristics = MigrateTransferCharacteristics(old.VideoTransferCharacteristics),
Height = old.Height,
Width = old.Width,
AudioBitrate = old.AudioBitrate,
RunTime = old.RunTime,
AudioStreamCount = old.AudioStreamCount,
VideoFps = old.VideoFps,
ScanType = old.ScanType,
AudioLanguages = MigrateLanguages(old.AudioLanguages),
Subtitles = MigrateLanguages(old.Subtitles)
};
m.VideoHdrFormat = MigrateHdrFormat(old);
MigrateVideoCodec(old, m, sceneName);
MigrateAudioCodec(old, m);
MigrateAudioChannelPositions(old, m);
m.AudioChannels = old.AudioChannelsStream > 0 ? old.AudioChannelsStream : old.AudioChannelsContainer;
return m;
}
private void MigrateVideoCodec(MediaInfo198 mediaInfo, MediaInfo199 m, string sceneName)
{
if (mediaInfo.VideoFormat == null)
{
MigrateVideoCodecLegacy(mediaInfo, m, sceneName);
return;
}
var videoFormat = mediaInfo.VideoFormat.Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries);
var videoCodecID = mediaInfo.VideoCodecID ?? string.Empty;
var videoCodecLibrary = mediaInfo.VideoCodecLibrary ?? string.Empty;
var result = mediaInfo.VideoFormat.Trim();
m.VideoFormat = result;
m.VideoCodecID = null;
if (videoFormat.ContainsIgnoreCase("x264"))
{
m.VideoFormat = "h264";
m.VideoCodecID = "x264";
return;
}
if (videoFormat.ContainsIgnoreCase("AVC") || videoFormat.ContainsIgnoreCase("V.MPEG4/ISO/AVC"))
{
m.VideoFormat = "h264";
if (videoCodecLibrary.StartsWithIgnoreCase("x264"))
{
m.VideoCodecID = "x264";
}
return;
}
if (videoFormat.ContainsIgnoreCase("HEVC") || videoFormat.ContainsIgnoreCase("V_MPEGH/ISO/HEVC"))
{
m.VideoFormat = "hevc";
if (videoCodecLibrary.StartsWithIgnoreCase("x265"))
{
m.VideoCodecID = "x265";
}
return;
}
if (videoFormat.ContainsIgnoreCase("MPEG Video"))
{
if (videoCodecID == "2" || videoCodecID == "V_MPEG2")
{
m.VideoFormat = "mpeg2video";
}
if (videoCodecID.IsNullOrWhiteSpace())
{
m.VideoFormat = "MPEG";
}
}
if (videoFormat.ContainsIgnoreCase("MPEG-2 Video"))
{
m.VideoFormat = "mpeg2video";
}
if (videoFormat.ContainsIgnoreCase("MPEG-4 Visual"))
{
m.VideoFormat = "mpeg4";
if (videoCodecID.ContainsIgnoreCase("XVID") ||
videoCodecLibrary.StartsWithIgnoreCase("XviD"))
{
m.VideoCodecID = "XVID";
}
if (videoCodecID.ContainsIgnoreCase("DIV3") ||
videoCodecID.ContainsIgnoreCase("DIVX") ||
videoCodecID.ContainsIgnoreCase("DX50") ||
videoCodecLibrary.StartsWithIgnoreCase("DivX"))
{
m.VideoCodecID = "DIVX";
}
return;
}
if (videoFormat.ContainsIgnoreCase("MPEG-4 Visual") || videoFormat.ContainsIgnoreCase("mp4v"))
{
m.VideoFormat = "mpeg4";
result = GetSceneNameMatch(sceneName, "XviD", "DivX", "");
if (result == "XviD")
{
m.VideoCodecID = "XVID";
}
if (result == "DivX")
{
m.VideoCodecID = "DIVX";
}
return;
}
if (videoFormat.ContainsIgnoreCase("VC-1"))
{
m.VideoFormat = "vc1";
return;
}
if (videoFormat.ContainsIgnoreCase("AV1"))
{
m.VideoFormat = "av1";
return;
}
if (videoFormat.ContainsIgnoreCase("VP6") || videoFormat.ContainsIgnoreCase("VP7") ||
videoFormat.ContainsIgnoreCase("VP8") || videoFormat.ContainsIgnoreCase("VP9"))
{
m.VideoFormat = videoFormat.First().ToLowerInvariant();
return;
}
if (videoFormat.ContainsIgnoreCase("WMV1") || videoFormat.ContainsIgnoreCase("WMV2"))
{
m.VideoFormat = "WMV";
return;
}
if (videoFormat.ContainsIgnoreCase("DivX") || videoFormat.ContainsIgnoreCase("div3"))
{
m.VideoFormat = "mpeg4";
m.VideoCodecID = "DIVX";
return;
}
if (videoFormat.ContainsIgnoreCase("XviD"))
{
m.VideoFormat = "mpeg4";
m.VideoCodecID = "XVID";
return;
}
if (videoFormat.ContainsIgnoreCase("V_QUICKTIME") ||
videoFormat.ContainsIgnoreCase("RealVideo 4"))
{
m.VideoFormat = "qtrle";
return;
}
if (videoFormat.ContainsIgnoreCase("mp42") ||
videoFormat.ContainsIgnoreCase("mp43"))
{
m.VideoFormat = "mpeg4";
return;
}
}
private void MigrateVideoCodecLegacy(MediaInfo198 mediaInfo, MediaInfo199 m, string sceneName)
{
var videoCodec = mediaInfo.VideoCodec;
m.VideoFormat = videoCodec;
m.VideoCodecID = null;
if (videoCodec.IsNullOrWhiteSpace())
{
m.VideoFormat = null;
return;
}
if (videoCodec == "AVC")
{
m.VideoFormat = "h264";
}
if (videoCodec == "V_MPEGH/ISO/HEVC" || videoCodec == "HEVC")
{
m.VideoFormat = "hevc";
}
if (videoCodec == "MPEG-2 Video")
{
m.VideoFormat = "mpeg2video";
}
if (videoCodec == "MPEG-4 Visual")
{
var result = GetSceneNameMatch(sceneName, "DivX", "XviD");
if (result == "DivX")
{
m.VideoFormat = "mpeg4";
m.VideoCodecID = "DIVX";
}
m.VideoFormat = "mpeg4";
m.VideoCodecID = "XVID";
}
if (videoCodec.StartsWithIgnoreCase("XviD"))
{
m.VideoFormat = "mpeg4";
m.VideoCodecID = "XVID";
}
if (videoCodec.StartsWithIgnoreCase("DivX"))
{
m.VideoFormat = "mpeg4";
m.VideoCodecID = "DIVX";
}
if (videoCodec.EqualsIgnoreCase("VC-1"))
{
m.VideoFormat = "vc1";
}
}
private HdrFormat MigrateHdrFormat(MediaInfo198 mediaInfo)
{
if (mediaInfo.VideoHdrFormatCompatibility.IsNotNullOrWhiteSpace())
{
if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("HLG"))
{
return HdrFormat.Hlg10;
}
if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("dolby"))
{
return HdrFormat.DolbyVision;
}
if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("dolby"))
{
return HdrFormat.DolbyVision;
}
if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("hdr10+"))
{
return HdrFormat.Hdr10Plus;
}
if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("hdr10"))
{
return HdrFormat.Hdr10;
}
}
return VideoFileInfoReader.GetHdrFormat(mediaInfo.VideoBitDepth, mediaInfo.VideoColourPrimaries, mediaInfo.VideoTransferCharacteristics, new ());
}
private void MigrateAudioCodec(MediaInfo198 mediaInfo, MediaInfo199 m)
{
if (mediaInfo.AudioCodecID == null)
{
MigrateAudioCodecLegacy(mediaInfo, m);
return;
}
var audioFormat = mediaInfo.AudioFormat.Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries);
var audioCodecID = mediaInfo.AudioCodecID ?? string.Empty;
var splitAdditionalFeatures = (mediaInfo.AudioAdditionalFeatures ?? string.Empty).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
m.AudioFormat = "";
if (audioFormat.Empty())
{
return;
}
if (audioFormat.ContainsIgnoreCase("Atmos"))
{
m.AudioFormat = "truehd";
m.AudioCodecID = "thd+";
return;
}
if (audioFormat.ContainsIgnoreCase("MLP FBA"))
{
m.AudioFormat = "truehd";
if (splitAdditionalFeatures.ContainsIgnoreCase("16-ch"))
{
m.AudioCodecID = "thd+";
return;
}
return;
}
if (audioFormat.ContainsIgnoreCase("TrueHD"))
{
m.AudioFormat = "truehd";
return;
}
if (audioFormat.ContainsIgnoreCase("FLAC"))
{
m.AudioFormat = "flac";
return;
}
if (audioFormat.ContainsIgnoreCase("DTS"))
{
m.AudioFormat = "dts";
if (splitAdditionalFeatures.ContainsIgnoreCase("XLL"))
{
if (splitAdditionalFeatures.ContainsIgnoreCase("X"))
{
m.AudioProfile = "DTS:X";
return;
}
m.AudioProfile = "DTS-HD MA";
return;
}
if (splitAdditionalFeatures.ContainsIgnoreCase("ES"))
{
m.AudioProfile = "DTS-ES";
return;
}
if (splitAdditionalFeatures.ContainsIgnoreCase("XBR"))
{
m.AudioProfile = "DTS-HD HRA";
return;
}
return;
}
if (audioFormat.ContainsIgnoreCase("E-AC-3"))
{
m.AudioFormat = "eac3";
if (splitAdditionalFeatures.ContainsIgnoreCase("JOC"))
{
m.AudioCodecID = "ec+3";
}
return;
}
if (audioFormat.ContainsIgnoreCase("AC-3"))
{
m.AudioFormat = "ac3";
return;
}
if (audioFormat.ContainsIgnoreCase("AAC"))
{
m.AudioFormat = "aac";
if (audioCodecID == "A_AAC/MPEG4/LC/SBR")
{
m.AudioCodecID = audioCodecID;
}
return;
}
if (audioFormat.ContainsIgnoreCase("mp3"))
{
m.AudioFormat = "mp3";
return;
}
if (audioFormat.ContainsIgnoreCase("MPEG Audio"))
{
if (mediaInfo.AudioCodecID == "55" || mediaInfo.AudioCodecID == "A_MPEG/L3" || mediaInfo.AudioProfile == "Layer 3")
{
m.AudioFormat = "mp3";
return;
}
if (mediaInfo.AudioCodecID == "A_MPEG/L2" || mediaInfo.AudioProfile == "Layer 2")
{
m.AudioFormat = "mp2";
}
}
if (audioFormat.ContainsIgnoreCase("Opus"))
{
m.AudioFormat = "opus";
return;
}
if (audioFormat.ContainsIgnoreCase("PCM"))
{
m.AudioFormat = "pcm_s16le";
return;
}
if (audioFormat.ContainsIgnoreCase("ADPCM"))
{
m.AudioFormat = "pcm_s16le";
return;
}
if (audioFormat.ContainsIgnoreCase("Vorbis"))
{
m.AudioFormat = "vorbis";
return;
}
if (audioFormat.ContainsIgnoreCase("WMA"))
{
m.AudioFormat = "wmav1";
return;
}
}
private void MigrateAudioCodecLegacy(MediaInfo198 mediaInfo, MediaInfo199 m)
{
var audioFormat = mediaInfo.AudioFormat;
m.AudioFormat = audioFormat;
if (audioFormat.IsNullOrWhiteSpace())
{
m.AudioFormat = null;
return;
}
if (audioFormat.EqualsIgnoreCase("AC-3"))
{
m.AudioFormat = "ac3";
return;
}
if (audioFormat.EqualsIgnoreCase("E-AC-3"))
{
m.AudioFormat = "eac3";
return;
}
if (audioFormat.EqualsIgnoreCase("AAC"))
{
m.AudioFormat = "aac";
return;
}
if (audioFormat.EqualsIgnoreCase("MPEG Audio") && mediaInfo.AudioProfile == "Layer 3")
{
m.AudioFormat = "mp3";
return;
}
if (audioFormat.EqualsIgnoreCase("DTS"))
{
m.AudioFormat = "DTS";
return;
}
if (audioFormat.EqualsIgnoreCase("TrueHD"))
{
m.AudioFormat = "truehd";
return;
}
if (audioFormat.EqualsIgnoreCase("FLAC"))
{
m.AudioFormat = "flac";
return;
}
if (audioFormat.EqualsIgnoreCase("Vorbis"))
{
m.AudioFormat = "vorbis";
return;
}
if (audioFormat.EqualsIgnoreCase("Opus"))
{
m.AudioFormat = "opus";
return;
}
}
private void MigrateAudioChannelPositions(MediaInfo198 mediaInfo, MediaInfo199 m)
{
var audioChannels = FormatAudioChannelsFromAudioChannelPositions(mediaInfo);
if (audioChannels == null || audioChannels == 0.0m)
{
audioChannels = FormatAudioChannelsFromAudioChannelPositionsText(mediaInfo);
}
if (audioChannels == null || audioChannels == 0.0m)
{
audioChannels = FormatAudioChannelsFromAudioChannels(mediaInfo);
}
audioChannels ??= 0;
m.AudioChannelPositions = audioChannels.ToString();
}
private decimal? FormatAudioChannelsFromAudioChannelPositions(MediaInfo198 mediaInfo)
{
var audioChannelPositions = mediaInfo.AudioChannelPositions;
if (audioChannelPositions.IsNullOrWhiteSpace())
{
return null;
}
try
{
if (audioChannelPositions.Contains("+"))
{
return audioChannelPositions.Split('+')
.Sum(s => decimal.Parse(s.Trim(), CultureInfo.InvariantCulture));
}
if (audioChannelPositions.Contains("/"))
{
var channelStringList = Regex.Replace(audioChannelPositions,
@"^\d+\sobjects",
"",
RegexOptions.Compiled | RegexOptions.IgnoreCase)
.Replace("Object Based / ", "")
.Split(new string[] { " / " }, StringSplitOptions.RemoveEmptyEntries)
.FirstOrDefault()
?.Split('/');
var positions = default(decimal);
if (channelStringList == null)
{
return 0;
}
foreach (var channel in channelStringList)
{
var channelSplit = channel.Split(new string[] { "." }, StringSplitOptions.None);
if (channelSplit.Length == 3)
{
positions += decimal.Parse(string.Format("{0}.{1}", channelSplit[1], channelSplit[2]), CultureInfo.InvariantCulture);
}
else
{
positions += decimal.Parse(channel, CultureInfo.InvariantCulture);
}
}
return positions;
}
}
catch
{
}
return null;
}
private decimal? FormatAudioChannelsFromAudioChannelPositionsText(MediaInfo198 mediaInfo)
{
var audioChannelPositionsTextContainer = mediaInfo.AudioChannelPositionsTextContainer;
var audioChannelPositionsTextStream = mediaInfo.AudioChannelPositionsTextStream;
var audioChannelsContainer = mediaInfo.AudioChannelsContainer;
var audioChannelsStream = mediaInfo.AudioChannelsStream;
//Skip if the positions texts give us nothing
if ((audioChannelPositionsTextContainer.IsNullOrWhiteSpace() || audioChannelPositionsTextContainer == "Object Based") &&
(audioChannelPositionsTextStream.IsNullOrWhiteSpace() || audioChannelPositionsTextStream == "Object Based"))
{
return null;
}
try
{
if (audioChannelsStream > 0)
{
return audioChannelPositionsTextStream.ContainsIgnoreCase("LFE") ? audioChannelsStream - 1 + 0.1m : audioChannelsStream;
}
return audioChannelPositionsTextContainer.ContainsIgnoreCase("LFE") ? audioChannelsContainer - 1 + 0.1m : audioChannelsContainer;
}
catch
{
}
return null;
}
private decimal? FormatAudioChannelsFromAudioChannels(MediaInfo198 mediaInfo)
{
var audioChannelsContainer = mediaInfo.AudioChannelsContainer;
var audioChannelsStream = mediaInfo.AudioChannelsStream;
var audioFormat = (mediaInfo.AudioFormat ?? string.Empty).Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries);
var splitAdditionalFeatures = (mediaInfo.AudioAdditionalFeatures ?? string.Empty).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
// Workaround https://github.com/MediaArea/MediaInfo/issues/299 for DTS-X Audio
if (audioFormat.ContainsIgnoreCase("DTS") &&
splitAdditionalFeatures.ContainsIgnoreCase("XLL") &&
splitAdditionalFeatures.ContainsIgnoreCase("X") &&
audioChannelsContainer > 0)
{
return audioChannelsContainer - 1 + 0.1m;
}
// FLAC 6 channels is likely 5.1
if (audioFormat.ContainsIgnoreCase("FLAC") && audioChannelsContainer == 6)
{
return 5.1m;
}
if (mediaInfo.SchemaRevision > 5)
{
return audioChannelsStream > 0 ? audioChannelsStream : audioChannelsContainer;
}
if (mediaInfo.SchemaRevision >= 3)
{
return audioChannelsContainer;
}
return null;
}
private List<string> MigrateLanguages(string mediaInfoLanguages)
{
var languages = new List<string>();
var tokens = mediaInfoLanguages.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
var cultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
for (int i = 0; i < tokens.Count; i++)
{
if (tokens[i] == "Swedis")
{
// Probably typo in mediainfo (should be 'Swedish')
languages.Add("swe");
continue;
}
if (tokens[i] == "Chinese" && OsInfo.IsNotWindows)
{
// Mono only has 'Chinese (Simplified)' & 'Chinese (Traditional)'
languages.Add("zho");
continue;
}
if (tokens[i] == "Norwegian")
{
languages.Add("nor");
continue;
}
try
{
var cultureInfo = cultures.FirstOrDefault(p => p.EnglishName.RemoveAccent() == tokens[i]);
if (cultureInfo != null)
{
languages.Add(cultureInfo.ThreeLetterISOLanguageName.ToLowerInvariant());
}
}
catch
{
}
}
return languages;
}
private string MigratePrimaries(string primary)
{
return primary.Replace("BT.", "bt");
}
private string MigrateTransferCharacteristics(string transferCharacteristics)
{
if (transferCharacteristics == "PQ")
{
return "smpte2084";
}
if (transferCharacteristics == "HLG")
{
return "arib-std-b67";
}
return "bt709";
}
private static string GetSceneNameMatch(string sceneName, params string[] tokens)
{
sceneName = sceneName.IsNotNullOrWhiteSpace() ? Parser.Parser.RemoveFileExtension(sceneName) : string.Empty;
foreach (var token in tokens)
{
if (sceneName.ContainsIgnoreCase(token))
{
return token;
}
}
// Last token is the default.
return tokens.Last();
}
public class MediaInfoRaw : ModelBase
{
public string MediaInfo { get; set; }
public string SceneName { get; set; }
}
public class MediaInfoBase
{
public int SchemaRevision { get; set; }
}
public class MediaInfo198 : MediaInfoBase
{
public string ContainerFormat { get; set; }
// Deprecated according to MediaInfo
public string VideoCodec { get; set; }
public string VideoFormat { get; set; }
public string VideoCodecID { get; set; }
public string VideoProfile { get; set; }
public string VideoCodecLibrary { get; set; }
public int VideoBitrate { get; set; }
public int VideoBitDepth { get; set; }
public int VideoMultiViewCount { get; set; }
public string VideoColourPrimaries { get; set; }
public string VideoTransferCharacteristics { get; set; }
public string VideoHdrFormat { get; set; }
public string VideoHdrFormatCompatibility { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public string AudioFormat { get; set; }
public string AudioCodecID { get; set; }
public string AudioCodecLibrary { get; set; }
public string AudioAdditionalFeatures { get; set; }
public int AudioBitrate { get; set; }
public TimeSpan RunTime { get; set; }
public int AudioStreamCount { get; set; }
public int AudioChannelsContainer { get; set; }
public int AudioChannelsStream { get; set; }
public string AudioChannelPositions { get; set; }
public string AudioChannelPositionsTextContainer { get; set; }
public string AudioChannelPositionsTextStream { get; set; }
public string AudioProfile { get; set; }
public decimal VideoFps { get; set; }
public string AudioLanguages { get; set; }
public string Subtitles { get; set; }
public string ScanType { get; set; }
}
public class MediaInfo199 : MediaInfoBase
{
public string ContainerFormat { get; set; }
public string VideoFormat { get; set; }
public string VideoCodecID { get; set; }
public string VideoProfile { get; set; }
public int VideoBitrate { get; set; }
public int VideoBitDepth { get; set; }
public int VideoMultiViewCount { get; set; }
public string VideoColourPrimaries { get; set; }
public string VideoTransferCharacteristics { get; set; }
public HdrFormat VideoHdrFormat { get; set; }
public int Height { get; set; }
public int Width { get; set; }
public string AudioFormat { get; set; }
public string AudioCodecID { get; set; }
public string AudioProfile { get; set; }
public int AudioBitrate { get; set; }
public TimeSpan RunTime { get; set; }
public int AudioStreamCount { get; set; }
public int AudioChannels { get; set; }
public string AudioChannelPositions { get; set; }
public decimal VideoFps { get; set; }
public List<string> AudioLanguages { get; set; }
public List<string> Subtitles { get; set; }
public string ScanType { get; set; }
}
}
}
@@ -307,14 +307,14 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
streamDetails.Add(video);
var audio = new XElement("audio");
var audioChannelCount = movieFile.MediaInfo.AudioChannelsStream > 0 ? movieFile.MediaInfo.AudioChannelsStream : movieFile.MediaInfo.AudioChannelsContainer;
var audioChannelCount = movieFile.MediaInfo.AudioChannels;
audio.Add(new XElement("bitrate", movieFile.MediaInfo.AudioBitrate));
audio.Add(new XElement("channels", audioChannelCount));
audio.Add(new XElement("codec", MediaInfoFormatter.FormatAudioCodec(movieFile.MediaInfo, sceneName)));
audio.Add(new XElement("language", movieFile.MediaInfo.AudioLanguages));
streamDetails.Add(audio);
if (movieFile.MediaInfo.Subtitles != null && movieFile.MediaInfo.Subtitles.Length > 0)
if (movieFile.MediaInfo.Subtitles != null && movieFile.MediaInfo.Subtitles.Count > 0)
{
var subtitle = new XElement("subtitle");
subtitle.Add(new XElement("language", movieFile.MediaInfo.Subtitles));
@@ -1,30 +0,0 @@
using System;
using System.Runtime.CompilerServices;
using NzbDrone.Core.Localization;
using NzbDrone.Core.MediaFiles.MediaInfo;
namespace NzbDrone.Core.HealthCheck.Checks
{
public class MediaInfoDllCheck : HealthCheckBase
{
public MediaInfoDllCheck(ILocalizationService localizationService)
: base(localizationService)
{
}
[MethodImpl(MethodImplOptions.NoOptimization)]
public override HealthCheck Check()
{
try
{
var mediaInfo = new MediaInfo();
}
catch (Exception e)
{
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("MediaInfoDllCheckMessage"), e.Message), "#mediainfo-not-loaded");
}
return new HealthCheck(GetType());
}
}
}
@@ -0,0 +1,12 @@
namespace NzbDrone.Core.MediaFiles.MediaInfo
{
public enum HdrFormat
{
None,
Pq10,
Hdr10,
Hdr10Plus,
Hlg10,
DolbyVision
}
}
@@ -1,4 +1,3 @@
using System;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
@@ -12,9 +11,9 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
{
public static class MediaInfoFormatter
{
private const string ValidHdrColourPrimaries = "BT.2020";
private const string VideoDynamicRangeHdr = "HDR";
private static readonly string[] ValidHdrTransferFunctions = { "PQ", "HLG" };
private static readonly Regex PositionRegex = new Regex(@"(?<position>^\d\.\d)", RegexOptions.Compiled);
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(MediaInfoFormatter));
@@ -24,101 +23,95 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
if (audioChannels == null || audioChannels == 0.0m)
{
audioChannels = FormatAudioChannelsFromAudioChannelPositionsText(mediaInfo);
audioChannels = mediaInfo.AudioChannels;
}
if (audioChannels == null || audioChannels == 0.0m)
{
audioChannels = FormatAudioChannelsFromAudioChannels(mediaInfo);
}
return audioChannels ?? 0;
return audioChannels.Value;
}
public static string FormatAudioCodec(MediaInfoModel mediaInfo, string sceneName)
{
if (mediaInfo.AudioCodecID == null)
if (mediaInfo.AudioFormat == null)
{
return FormatAudioCodecLegacy(mediaInfo, sceneName);
return null;
}
var audioFormat = mediaInfo.AudioFormat.Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries);
var audioFormat = mediaInfo.AudioFormat;
var audioCodecID = mediaInfo.AudioCodecID ?? string.Empty;
var audioProfile = mediaInfo.AudioProfile ?? string.Empty;
var audioCodecLibrary = mediaInfo.AudioCodecLibrary ?? string.Empty;
var splitAdditionalFeatures = (mediaInfo.AudioAdditionalFeatures ?? string.Empty).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (audioFormat.Empty())
{
return string.Empty;
}
if (audioFormat.ContainsIgnoreCase("Atmos"))
// see definitions here https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/codec_desc.c
if (audioCodecID == "thd+")
{
return "TrueHD Atmos";
}
if (audioFormat.ContainsIgnoreCase("MLP FBA"))
{
if (splitAdditionalFeatures.ContainsIgnoreCase("16-ch"))
{
return "TrueHD Atmos";
}
return "TrueHD";
}
if (audioFormat.ContainsIgnoreCase("TrueHD"))
if (audioFormat == "truehd")
{
return "TrueHD";
}
if (audioFormat.ContainsIgnoreCase("FLAC"))
if (audioFormat == "flac")
{
return "FLAC";
}
if (audioFormat.ContainsIgnoreCase("DTS"))
if (audioFormat == "dts")
{
if (splitAdditionalFeatures.ContainsIgnoreCase("XLL"))
if (audioProfile == "DTS:X")
{
if (splitAdditionalFeatures.ContainsIgnoreCase("X"))
{
return "DTS-X";
}
return "DTS-X";
}
if (audioProfile == "DTS-HD MA")
{
return "DTS-HD MA";
}
if (splitAdditionalFeatures.ContainsIgnoreCase("ES"))
if (audioProfile == "DTS-ES")
{
return "DTS-ES";
}
if (splitAdditionalFeatures.ContainsIgnoreCase("XBR"))
if (audioProfile == "DTS-HD HRA")
{
return "DTS-HD HRA";
}
if (audioProfile == "DTS Express")
{
return "DTS Express";
}
if (audioProfile == "DTS 96/24")
{
return "DTS 96/24";
}
return "DTS";
}
if (audioFormat.ContainsIgnoreCase("E-AC-3"))
if (audioCodecID == "ec+3")
{
if (splitAdditionalFeatures.ContainsIgnoreCase("JOC"))
{
return "EAC3 Atmos";
}
return "EAC3 Atmos";
}
if (audioFormat == "eac3")
{
return "EAC3";
}
if (audioFormat.ContainsIgnoreCase("AC-3"))
if (audioFormat == "ac3")
{
return "AC3";
}
if (audioFormat.ContainsIgnoreCase("AAC"))
if (audioFormat == "aac")
{
if (audioCodecID == "A_AAC/MPEG4/LC/SBR")
{
@@ -128,461 +121,166 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
return "AAC";
}
if (audioFormat.ContainsIgnoreCase("mp3"))
if (audioFormat == "mp3")
{
return "MP3";
}
if (audioFormat.ContainsIgnoreCase("MPEG Audio"))
if (audioFormat == "mp2")
{
if (mediaInfo.AudioCodecID == "55" || mediaInfo.AudioCodecID == "A_MPEG/L3" || mediaInfo.AudioProfile == "Layer 3")
{
return "MP3";
}
if (mediaInfo.AudioCodecID == "A_MPEG/L2" || mediaInfo.AudioProfile == "Layer 2")
{
return "MP2";
}
return "MP2";
}
if (audioFormat.ContainsIgnoreCase("Opus"))
if (audioFormat == "opus")
{
return "Opus";
}
if (audioFormat.ContainsIgnoreCase("PCM"))
if (audioFormat.StartsWith("pcm_") || audioFormat.StartsWith("adpcm_"))
{
return "PCM";
}
if (audioFormat.ContainsIgnoreCase("ADPCM"))
{
return "PCM";
}
if (audioFormat.ContainsIgnoreCase("Vorbis"))
if (audioFormat == "vorbis")
{
return "Vorbis";
}
if (audioFormat.ContainsIgnoreCase("WMA"))
if (audioFormat == "wmav1" ||
audioFormat == "wmav2")
{
return "WMA";
}
if (audioFormat.ContainsIgnoreCase("A_QUICKTIME"))
{
return "";
}
Logger.Debug()
.Message("Unknown audio format: '{0}' in '{1}'.", string.Join(", ", mediaInfo.AudioFormat, audioCodecID, audioProfile, audioCodecLibrary, mediaInfo.AudioAdditionalFeatures), sceneName)
.WriteSentryWarn("UnknownAudioFormat", mediaInfo.ContainerFormat, mediaInfo.AudioFormat, audioCodecID)
.Message("Unknown audio format: '{0}' in '{1}'.", mediaInfo.RawStreamData, sceneName)
.WriteSentryWarn("UnknownAudioFormatFFProbe", mediaInfo.ContainerFormat, mediaInfo.AudioFormat, audioCodecID)
.Write();
return mediaInfo.AudioFormat;
}
public static string FormatAudioCodecLegacy(MediaInfoModel mediaInfo, string sceneName)
{
var audioFormat = mediaInfo.AudioFormat;
if (audioFormat.IsNullOrWhiteSpace())
{
return audioFormat;
}
if (audioFormat.EqualsIgnoreCase("AC-3"))
{
return "AC3";
}
if (audioFormat.EqualsIgnoreCase("E-AC-3"))
{
return "EAC3";
}
if (audioFormat.EqualsIgnoreCase("AAC"))
{
return "AAC";
}
if (audioFormat.EqualsIgnoreCase("MPEG Audio") && mediaInfo.AudioProfile == "Layer 3")
{
return "MP3";
}
if (audioFormat.EqualsIgnoreCase("DTS"))
{
return "DTS";
}
if (audioFormat.EqualsIgnoreCase("TrueHD"))
{
return "TrueHD";
}
if (audioFormat.EqualsIgnoreCase("FLAC"))
{
return "FLAC";
}
if (audioFormat.EqualsIgnoreCase("Vorbis"))
{
return "Vorbis";
}
if (audioFormat.EqualsIgnoreCase("Opus"))
{
return "Opus";
}
return audioFormat;
}
public static string FormatVideoCodec(MediaInfoModel mediaInfo, string sceneName)
{
if (mediaInfo.VideoFormat == null)
{
return FormatVideoCodecLegacy(mediaInfo, sceneName);
return null;
}
var videoFormat = mediaInfo.VideoFormat.Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries);
var videoFormat = mediaInfo.VideoFormat;
var videoCodecID = mediaInfo.VideoCodecID ?? string.Empty;
var videoProfile = mediaInfo.VideoProfile ?? string.Empty;
var videoCodecLibrary = mediaInfo.VideoCodecLibrary ?? string.Empty;
var result = mediaInfo.VideoFormat.Trim();
var result = videoFormat.Trim();
if (videoFormat.Empty())
{
return result;
}
if (videoFormat.ContainsIgnoreCase("x264"))
// see definitions here: https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/codec_desc.c
if (videoCodecID == "x264")
{
return "x264";
}
if (videoFormat.ContainsIgnoreCase("AVC") || videoFormat.ContainsIgnoreCase("V.MPEG4/ISO/AVC"))
if (videoFormat == "h264")
{
if (videoCodecLibrary.StartsWithIgnoreCase("x264"))
{
return "x264";
}
return GetSceneNameMatch(sceneName, "AVC", "x264", "h264");
}
if (videoFormat.ContainsIgnoreCase("HEVC") || videoFormat.ContainsIgnoreCase("V_MPEGH/ISO/HEVC"))
if (videoCodecID == "x265")
{
if (videoCodecLibrary.StartsWithIgnoreCase("x265"))
{
return "x265";
}
return "x265";
}
if (videoFormat == "hevc")
{
return GetSceneNameMatch(sceneName, "HEVC", "x265", "h265");
}
if (videoFormat.ContainsIgnoreCase("MPEG Video"))
{
if (videoCodecID == "2" || videoCodecID == "V_MPEG2")
{
return "MPEG2";
}
if (videoCodecID.IsNullOrWhiteSpace())
{
return "MPEG";
}
}
if (videoFormat.ContainsIgnoreCase("MPEG-2 Video"))
if (videoFormat == "mpeg2video")
{
return "MPEG2";
}
if (videoFormat.ContainsIgnoreCase("MPEG-4 Visual"))
if (videoFormat == "mpeg1video")
{
if (videoCodecID.ContainsIgnoreCase("XVID") ||
videoCodecLibrary.StartsWithIgnoreCase("XviD"))
return "MPEG";
}
if (videoFormat == "mpeg4")
{
if (videoCodecID == "XVID")
{
return "XviD";
}
if (videoCodecID.ContainsIgnoreCase("DIV3") ||
videoCodecID.ContainsIgnoreCase("DIVX") ||
videoCodecID.ContainsIgnoreCase("DX50") ||
videoCodecLibrary.StartsWithIgnoreCase("DivX"))
if (videoCodecID == "DIV3" ||
videoCodecID == "DIVX" ||
videoCodecID == "DX50")
{
return "DivX";
}
return "";
}
if (videoFormat.ContainsIgnoreCase("MPEG-4 Visual") || videoFormat.ContainsIgnoreCase("mp4v"))
{
result = GetSceneNameMatch(sceneName, "XviD", "DivX", "");
if (result.IsNotNullOrWhiteSpace())
{
return result;
}
if (videoCodecLibrary.Contains("Lavc"))
{
return ""; // libavcodec mpeg-4
}
if (videoCodecLibrary.Contains("em4v"))
{
return ""; // NeroDigital
}
if (videoCodecLibrary.Contains("Intel(R) IPP"))
{
return ""; // Intel(R) IPP
}
if (videoCodecLibrary.Contains("ZJMedia") ||
videoCodecLibrary.Contains("DigiArty"))
{
return ""; // Other
}
if (string.IsNullOrEmpty(videoCodecLibrary))
{
return ""; // Unknown mp4v
}
}
if (videoFormat.ContainsIgnoreCase("VC-1"))
if (videoFormat == "vc1")
{
return "VC1";
}
if (videoFormat.ContainsIgnoreCase("AV1"))
if (videoFormat == "av1")
{
return "AV1";
}
if (videoFormat.ContainsIgnoreCase("VP6") || videoFormat.ContainsIgnoreCase("VP7") ||
videoFormat.ContainsIgnoreCase("VP8") || videoFormat.ContainsIgnoreCase("VP9"))
if (videoFormat == "vp6" ||
videoFormat == "vp7" ||
videoFormat == "vp8" ||
videoFormat == "vp9")
{
return videoFormat.First().ToUpperInvariant();
return videoFormat.ToUpperInvariant();
}
if (videoFormat.ContainsIgnoreCase("WMV1") || videoFormat.ContainsIgnoreCase("WMV2"))
if (videoFormat == "wmv1" ||
videoFormat == "wmv2")
{
return "WMV";
}
if (videoFormat.ContainsIgnoreCase("DivX") || videoFormat.ContainsIgnoreCase("div3"))
if (videoFormat == "qtrle" ||
videoFormat == "rpza" ||
videoFormat == "rv10" ||
videoFormat == "rv20" ||
videoFormat == "rv30" ||
videoFormat == "rv40")
{
return "DivX";
}
if (videoFormat.ContainsIgnoreCase("XviD"))
{
return "XviD";
}
if (videoFormat.ContainsIgnoreCase("V_QUICKTIME") ||
videoFormat.ContainsIgnoreCase("RealVideo 4"))
{
return "";
}
if (videoFormat.ContainsIgnoreCase("mp42") ||
videoFormat.ContainsIgnoreCase("mp43"))
{
// MS old DivX competitor
return "";
}
Logger.Debug()
.Message("Unknown video format: '{0}' in '{1}'.", string.Join(", ", mediaInfo.VideoFormat, videoCodecID, videoProfile, videoCodecLibrary), sceneName)
.WriteSentryWarn("UnknownVideoFormat", mediaInfo.ContainerFormat, mediaInfo.VideoFormat, videoCodecID)
.Message("Unknown video format: '{0}' in '{1}'.", mediaInfo.RawStreamData, sceneName)
.WriteSentryWarn("UnknownVideoFormatFFProbe", mediaInfo.ContainerFormat, videoFormat, videoCodecID)
.Write();
return result;
}
public static string FormatVideoCodecLegacy(MediaInfoModel mediaInfo, string sceneName)
{
var videoCodec = mediaInfo.VideoCodec;
if (videoCodec.IsNullOrWhiteSpace())
{
return videoCodec;
}
if (videoCodec == "AVC")
{
return GetSceneNameMatch(sceneName, "AVC", "h264", "x264");
}
if (videoCodec == "V_MPEGH/ISO/HEVC" || videoCodec == "HEVC")
{
return GetSceneNameMatch(sceneName, "HEVC", "h265", "x265");
}
if (videoCodec == "MPEG-2 Video")
{
return "MPEG2";
}
if (videoCodec == "MPEG-4 Visual")
{
return GetSceneNameMatch(sceneName, "DivX", "XviD");
}
if (videoCodec.StartsWithIgnoreCase("XviD"))
{
return "XviD";
}
if (videoCodec.StartsWithIgnoreCase("DivX"))
{
return "DivX";
}
if (videoCodec.EqualsIgnoreCase("VC-1"))
{
return "VC1";
}
return videoCodec;
}
private static decimal? FormatAudioChannelsFromAudioChannelPositions(MediaInfoModel mediaInfo)
{
var audioChannelPositions = mediaInfo.AudioChannelPositions;
var audioFormat = mediaInfo.AudioFormat;
if (audioChannelPositions.IsNullOrWhiteSpace())
if (mediaInfo.AudioChannelPositions == null)
{
return null;
return 0;
}
try
var match = PositionRegex.Match(mediaInfo.AudioChannelPositions);
if (match.Success)
{
if (audioChannelPositions.Contains("+"))
{
return audioChannelPositions.Split('+')
.Sum(s => decimal.Parse(s.Trim(), CultureInfo.InvariantCulture));
}
if (audioChannelPositions.Contains("/"))
{
var channelStringList = Regex.Replace(audioChannelPositions,
@"^\d+\sobjects",
"",
RegexOptions.Compiled | RegexOptions.IgnoreCase)
.Replace("Object Based / ", "")
.Split(new string[] { " / " }, StringSplitOptions.RemoveEmptyEntries)
.FirstOrDefault()
?.Split('/');
var positions = default(decimal);
if (channelStringList == null)
{
return 0;
}
foreach (var channel in channelStringList)
{
var channelSplit = channel.Split(new string[] { "." }, StringSplitOptions.None);
if (channelSplit.Length == 3)
{
positions += decimal.Parse(string.Format("{0}.{1}", channelSplit[1], channelSplit[2]), CultureInfo.InvariantCulture);
}
else
{
positions += decimal.Parse(channel, CultureInfo.InvariantCulture);
}
}
return positions;
}
}
catch (Exception ex)
{
Logger.Warn()
.Message("Unable to format audio channels using 'AudioChannelPositions', with a value of: '{0}' and '{1}'. Error {2}", audioChannelPositions, mediaInfo.AudioChannelPositionsTextContainer, ex.Message)
.WriteSentryWarn("UnknownAudioChannelFormat", audioChannelPositions, mediaInfo.AudioChannelPositionsTextContainer)
.Write();
return decimal.Parse(match.Groups["position"].Value, NumberStyles.Number, CultureInfo.InvariantCulture);
}
return null;
}
private static decimal? FormatAudioChannelsFromAudioChannelPositionsText(MediaInfoModel mediaInfo)
{
var audioChannelPositionsTextContainer = mediaInfo.AudioChannelPositionsTextContainer;
var audioChannelPositionsTextStream = mediaInfo.AudioChannelPositionsTextStream;
var audioChannelsContainer = mediaInfo.AudioChannelsContainer;
var audioChannelsStream = mediaInfo.AudioChannelsStream;
//Skip if the positions texts give us nothing
if ((audioChannelPositionsTextContainer.IsNullOrWhiteSpace() || audioChannelPositionsTextContainer == "Object Based") &&
(audioChannelPositionsTextStream.IsNullOrWhiteSpace() || audioChannelPositionsTextStream == "Object Based"))
{
return null;
}
try
{
if (audioChannelsStream > 0)
{
return audioChannelPositionsTextStream.ContainsIgnoreCase("LFE") ? audioChannelsStream - 1 + 0.1m : audioChannelsStream;
}
return audioChannelPositionsTextContainer.ContainsIgnoreCase("LFE") ? audioChannelsContainer - 1 + 0.1m : audioChannelsContainer;
}
catch (Exception e)
{
Logger.Warn(e, "Unable to format audio channels using 'AudioChannelPositionsText' or 'AudioChannelPositionsTextStream', with value of: '{0}' and '{1}", audioChannelPositionsTextContainer, audioChannelPositionsTextStream);
}
return null;
}
private static decimal? FormatAudioChannelsFromAudioChannels(MediaInfoModel mediaInfo)
{
var audioChannelsContainer = mediaInfo.AudioChannelsContainer;
var audioChannelsStream = mediaInfo.AudioChannelsStream;
var audioFormat = (mediaInfo.AudioFormat ?? string.Empty).Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries);
var splitAdditionalFeatures = (mediaInfo.AudioAdditionalFeatures ?? string.Empty).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
// Workaround https://github.com/MediaArea/MediaInfo/issues/299 for DTS-X Audio
if (audioFormat.ContainsIgnoreCase("DTS") &&
splitAdditionalFeatures.ContainsIgnoreCase("XLL") &&
splitAdditionalFeatures.ContainsIgnoreCase("X") &&
audioChannelsContainer > 0)
{
return audioChannelsContainer - 1 + 0.1m;
}
// FLAC 6 channels is likely 5.1
if (audioFormat.ContainsIgnoreCase("FLAC") && audioChannelsContainer == 6)
{
return 5.1m;
}
if (mediaInfo.SchemaRevision > 5)
{
return audioChannelsStream > 0 ? audioChannelsStream : audioChannelsContainer;
}
if (mediaInfo.SchemaRevision >= 3)
{
return audioChannelsContainer;
}
return null;
return 0;
}
private static string GetSceneNameMatch(string sceneName, params string[] tokens)
@@ -603,23 +301,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
public static string FormatVideoDynamicRange(MediaInfoModel mediaInfo)
{
if (mediaInfo.VideoHdrFormat.IsNotNullOrWhiteSpace())
{
return VideoDynamicRangeHdr;
}
if (mediaInfo.VideoBitDepth >= 10 &&
mediaInfo.VideoColourPrimaries.IsNotNullOrWhiteSpace() &&
mediaInfo.VideoTransferCharacteristics.IsNotNullOrWhiteSpace())
{
if (mediaInfo.VideoColourPrimaries.EqualsIgnoreCase(ValidHdrColourPrimaries) &&
ValidHdrTransferFunctions.Any(mediaInfo.VideoTransferCharacteristics.Contains))
{
return VideoDynamicRangeHdr;
}
}
return "";
return mediaInfo.VideoHdrFormat != HdrFormat.None ? VideoDynamicRangeHdr : "";
}
}
}
@@ -1,370 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Core.MediaFiles.MediaInfo
{
[Flags]
public enum BufferStatus
{
Accepted = 1,
Filled = 2,
Updated = 4,
Finalized = 8
}
public enum StreamKind
{
General,
Video,
Audio,
Text,
Other,
Image,
Menu
}
public enum InfoKind
{
Name,
Text,
Measure,
Options,
NameText,
MeasureText,
Info,
HowTo
}
public enum InfoOptions
{
ShowInInform,
Support,
ShowInSupported,
TypeOfValue
}
public enum InfoFileOptions
{
FileOption_Nothing = 0x00,
FileOption_NoRecursive = 0x01,
FileOption_CloseAll = 0x02,
FileOption_Max = 0x04
}
public class MediaInfo : IDisposable
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(MediaInfo));
private IntPtr _handle;
public bool MustUseAnsi { get; set; }
public Encoding Encoding { get; set; }
public MediaInfo()
{
_handle = MediaInfo_New();
InitializeEncoding();
}
~MediaInfo()
{
if (_handle != IntPtr.Zero)
{
MediaInfo_Delete(_handle);
}
}
public void Dispose()
{
if (_handle != IntPtr.Zero)
{
MediaInfo_Delete(_handle);
}
GC.SuppressFinalize(this);
}
private void InitializeEncoding()
{
if (Environment.OSVersion.ToString().IndexOf("Windows") != -1)
{
// Windows guaranteed UCS-2
MustUseAnsi = false;
Encoding = Encoding.Unicode;
}
else
{
var responses = new List<string>();
// Linux normally UCS-4. As fallback we try UCS-2 and plain Ansi.
MustUseAnsi = false;
Encoding = Encoding.UTF32;
var version = Option("Info_Version", "");
responses.Add(version);
if (version.StartsWith("MediaInfoLib"))
{
return;
}
Encoding = Encoding.Unicode;
version = Option("Info_Version", "");
responses.Add(version);
if (version.StartsWith("MediaInfoLib"))
{
return;
}
MustUseAnsi = true;
Encoding = Encoding.Default;
version = Option("Info_Version", "");
responses.Add(version);
if (version.StartsWith("MediaInfoLib"))
{
return;
}
throw new NotSupportedException("Unsupported MediaInfoLib encoding, version check responses (may be gibberish, show it to the Radarr devs): " + responses.Join(", "));
}
}
private IntPtr MakeStringParameter(string value)
{
var buffer = Encoding.GetBytes(value);
Array.Resize(ref buffer, buffer.Length + 4);
var buf = Marshal.AllocHGlobal(buffer.Length);
Marshal.Copy(buffer, 0, buf, buffer.Length);
return buf;
}
private string MakeStringResult(IntPtr value)
{
if (Encoding == Encoding.Unicode)
{
return Marshal.PtrToStringUni(value);
}
else if (Encoding == Encoding.UTF32)
{
int i = 0;
for (; i < 1024; i += 4)
{
var data = Marshal.ReadInt32(value, i);
if (data == 0)
{
break;
}
}
var buffer = new byte[i];
Marshal.Copy(value, buffer, 0, i);
return Encoding.GetString(buffer, 0, i);
}
else
{
return Marshal.PtrToStringAnsi(value);
}
}
public int Open(string fileName)
{
var pFileName = MakeStringParameter(fileName);
try
{
if (MustUseAnsi)
{
return (int)MediaInfoA_Open(_handle, pFileName);
}
else
{
return (int)MediaInfo_Open(_handle, pFileName);
}
}
finally
{
Marshal.FreeHGlobal(pFileName);
}
}
public int Open(Stream stream)
{
if (stream.Length < 1024)
{
return 0;
}
var isValid = (int)MediaInfo_Open_Buffer_Init(_handle, stream.Length, 0);
if (isValid == 1)
{
var buffer = new byte[16 * 1024];
long seekStart = 0;
long totalRead = 0;
int bufferRead;
do
{
bufferRead = stream.Read(buffer, 0, buffer.Length);
totalRead += bufferRead;
var status = (BufferStatus)MediaInfo_Open_Buffer_Continue(_handle, buffer, (IntPtr)bufferRead);
if (status.HasFlag(BufferStatus.Finalized) || status <= 0 || bufferRead == 0)
{
Logger.Trace("Read file offset {0}-{1} ({2} bytes)", seekStart, stream.Position, stream.Position - seekStart);
break;
}
var seekPos = MediaInfo_Open_Buffer_Continue_GoTo_Get(_handle);
if (seekPos != -1)
{
Logger.Trace("Read file offset {0}-{1} ({2} bytes)", seekStart, stream.Position, stream.Position - seekStart);
seekPos = stream.Seek(seekPos, SeekOrigin.Begin);
seekStart = seekPos;
MediaInfo_Open_Buffer_Init(_handle, stream.Length, seekPos);
}
}
while (bufferRead > 0);
MediaInfo_Open_Buffer_Finalize(_handle);
Logger.Trace("Read a total of {0} bytes ({1:0.0}%)", totalRead, totalRead * 100.0 / stream.Length);
}
return isValid;
}
public void Close()
{
MediaInfo_Close(_handle);
}
public string Get(StreamKind streamKind, int streamNumber, string parameter, InfoKind infoKind = InfoKind.Text, InfoKind searchKind = InfoKind.Name)
{
var pParameter = MakeStringParameter(parameter);
try
{
if (MustUseAnsi)
{
return MakeStringResult(MediaInfoA_Get(_handle, (IntPtr)streamKind, (IntPtr)streamNumber, pParameter, (IntPtr)infoKind, (IntPtr)searchKind));
}
else
{
return MakeStringResult(MediaInfo_Get(_handle, (IntPtr)streamKind, (IntPtr)streamNumber, pParameter, (IntPtr)infoKind, (IntPtr)searchKind));
}
}
finally
{
Marshal.FreeHGlobal(pParameter);
}
}
public string Get(StreamKind streamKind, int streamNumber, int parameter, InfoKind infoKind)
{
if (MustUseAnsi)
{
return MakeStringResult(MediaInfoA_GetI(_handle, (IntPtr)streamKind, (IntPtr)streamNumber, (IntPtr)parameter, (IntPtr)infoKind));
}
else
{
return MakeStringResult(MediaInfo_GetI(_handle, (IntPtr)streamKind, (IntPtr)streamNumber, (IntPtr)parameter, (IntPtr)infoKind));
}
}
public string Option(string option, string value)
{
var pOption = MakeStringParameter(option.ToLowerInvariant());
var pValue = MakeStringParameter(value);
try
{
if (MustUseAnsi)
{
return MakeStringResult(MediaInfoA_Option(_handle, pOption, pValue));
}
else
{
return MakeStringResult(MediaInfo_Option(_handle, pOption, pValue));
}
}
finally
{
Marshal.FreeHGlobal(pOption);
Marshal.FreeHGlobal(pValue);
}
}
public int State_Get()
{
return (int)MediaInfo_State_Get(_handle);
}
public int Count_Get(StreamKind streamKind, int streamNumber = -1)
{
return (int)MediaInfo_Count_Get(_handle, (IntPtr)streamKind, (IntPtr)streamNumber);
}
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_New();
[DllImport("mediainfo")]
private static extern void MediaInfo_Delete(IntPtr handle);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_Open(IntPtr handle, IntPtr fileName);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_Open_Buffer_Init(IntPtr handle, long fileSize, long fileOffset);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_Open_Buffer_Continue(IntPtr handle, byte[] buffer, IntPtr bufferSize);
[DllImport("mediainfo")]
private static extern long MediaInfo_Open_Buffer_Continue_GoTo_Get(IntPtr handle);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_Open_Buffer_Finalize(IntPtr handle);
[DllImport("mediainfo")]
private static extern void MediaInfo_Close(IntPtr handle);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_GetI(IntPtr handle, IntPtr streamKind, IntPtr streamNumber, IntPtr parameter, IntPtr infoKind);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_Get(IntPtr handle, IntPtr streamKind, IntPtr streamNumber, IntPtr parameter, IntPtr infoKind, IntPtr searchKind);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_Option(IntPtr handle, IntPtr option, IntPtr value);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_State_Get(IntPtr handle);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_Count_Get(IntPtr handle, IntPtr streamKind, IntPtr streamNumber);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_New();
[DllImport("mediainfo")]
private static extern void MediaInfoA_Delete(IntPtr handle);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_Open(IntPtr handle, IntPtr fileName);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_Open_Buffer_Init(IntPtr handle, long fileSize, long fileOffset);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_Open_Buffer_Continue(IntPtr handle, byte[] buffer, IntPtr bufferSize);
[DllImport("mediainfo")]
private static extern long MediaInfoA_Open_Buffer_Continue_GoTo_Get(IntPtr handle);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_Open_Buffer_Finalize(IntPtr handle);
[DllImport("mediainfo")]
private static extern void MediaInfoA_Close(IntPtr handle);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_GetI(IntPtr handle, IntPtr streamKind, IntPtr streamNumber, IntPtr parameter, IntPtr infoKind);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_Get(IntPtr handle, IntPtr streamKind, IntPtr streamNumber, IntPtr parameter, IntPtr infoKind, IntPtr searchKind);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_Option(IntPtr handle, IntPtr option, IntPtr value);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_State_Get(IntPtr handle);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_Count_Get(IntPtr handle, IntPtr streamKind, IntPtr streamNumber);
}
}
@@ -1,44 +1,70 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using FFMpegCore;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.MediaFiles.MediaInfo
{
public class MediaInfoModel : IEmbeddedDocument
{
public string ContainerFormat { get; set; }
// Deprecated according to MediaInfo
public string VideoCodec { get; set; }
public string VideoFormat { get; set; }
public string VideoCodecID { get; set; }
public string VideoProfile { get; set; }
public string VideoCodecLibrary { get; set; }
public int VideoBitrate { get; set; }
public int VideoBitDepth { get; set; }
public int VideoMultiViewCount { get; set; }
public string VideoColourPrimaries { get; set; }
public string VideoTransferCharacteristics { get; set; }
public string VideoHdrFormat { get; set; }
public string VideoHdrFormatCompatibility { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public string AudioFormat { get; set; }
public string AudioCodecID { get; set; }
public string AudioCodecLibrary { get; set; }
public string AudioAdditionalFeatures { get; set; }
public int AudioBitrate { get; set; }
public TimeSpan RunTime { get; set; }
public int AudioStreamCount { get; set; }
public int AudioChannelsContainer { get; set; }
public int AudioChannelsStream { get; set; }
public string AudioChannelPositions { get; set; }
public string AudioChannelPositionsTextContainer { get; set; }
public string AudioChannelPositionsTextStream { get; set; }
public string AudioProfile { get; set; }
public decimal VideoFps { get; set; }
public string AudioLanguages { get; set; }
public string Subtitles { get; set; }
public string ScanType { get; set; }
public string RawStreamData { get; set; }
public string RawFrameData { get; set; }
public int SchemaRevision { get; set; }
[JsonIgnore]
public IMediaAnalysis Analysis => FFProbe.Analyse(RawStreamData);
[JsonIgnore]
public IMediaAnalysis Frames => FFProbe.Analyse(RawFrameData);
public string ContainerFormat { get; set; }
public string VideoFormat { get; set; }
public string VideoCodecID { get; set; }
public string VideoProfile { get; set; }
public int VideoBitrate { get; set; }
public int VideoBitDepth { get; set; }
public int VideoMultiViewCount { get; set; }
public string VideoColourPrimaries { get; set; }
public string VideoTransferCharacteristics { get; set; }
public DoviConfigurationRecordSideData DoviConfigurationRecord { get; set; }
public HdrFormat VideoHdrFormat { get; set; }
public int Height { get; set; }
public int Width { get; set; }
public string AudioFormat { get; set; }
public string AudioCodecID { get; set; }
public string AudioProfile { get; set; }
public int AudioBitrate { get; set; }
public TimeSpan RunTime { get; set; }
public int AudioStreamCount { get; set; }
public int AudioChannels { get; set; }
public string AudioChannelPositions { get; set; }
public decimal VideoFps { get; set; }
public List<string> AudioLanguages { get; set; }
public List<string> Subtitles { get; set; }
public string ScanType { get; set; }
}
}
@@ -1,6 +1,8 @@
using System;
using System.Globalization;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FFMpegCore;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
@@ -17,14 +19,33 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
{
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger;
private readonly List<FFProbePixelFormat> _pixelFormats;
public const int MINIMUM_MEDIA_INFO_SCHEMA_REVISION = 4;
public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 7;
public const int MINIMUM_MEDIA_INFO_SCHEMA_REVISION = 8;
public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 8;
private static readonly string[] ValidHdrColourPrimaries = { "bt2020" };
private static readonly string[] HlgTransferFunctions = { "bt2020-10", "arib-std-b67" };
private static readonly string[] PqTransferFunctions = { "smpte2084" };
private static readonly string[] ValidHdrTransferFunctions = HlgTransferFunctions.Concat(PqTransferFunctions).ToArray();
public VideoFileInfoReader(IDiskProvider diskProvider, Logger logger)
{
_diskProvider = diskProvider;
_logger = logger;
// We bundle ffprobe for all platforms
GlobalFFOptions.Configure(options => options.BinaryFolder = AppDomain.CurrentDomain.BaseDirectory);
try
{
_pixelFormats = FFProbe.GetPixelFormats();
}
catch (Exception e)
{
_logger.Error(e, "Failed to get supported pixel formats from ffprobe");
_pixelFormats = new List<FFProbePixelFormat>();
}
}
public MediaInfoModel GetMediaInfo(string filename)
@@ -34,181 +55,77 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
throw new FileNotFoundException("Media file does not exist: " + filename);
}
MediaInfo mediaInfo = null;
// TODO: Cache media info by path, mtime and length so we don't need to read files multiple times
try
{
mediaInfo = new MediaInfo();
_logger.Debug("Getting media info from {0}", filename);
var ffprobeOutput = FFProbe.GetStreamJson(filename, ffOptions: new FFOptions { ExtraArguments = "-probesize 50000000" });
if (filename.ToLower().EndsWith(".ts"))
var analysis = FFProbe.AnalyseStreamJson(ffprobeOutput);
if (analysis.PrimaryAudioStream.ChannelLayout.IsNullOrWhiteSpace())
{
// For .ts files we often have to scan more of the file to get all the info we need
mediaInfo.Option("ParseSpeed", "0.3");
}
else
{
mediaInfo.Option("ParseSpeed", "0.0");
ffprobeOutput = FFProbe.GetStreamJson(filename, ffOptions: new FFOptions { ExtraArguments = "-probesize 150000000 -analyzeduration 150000000" });
analysis = FFProbe.AnalyseStreamJson(ffprobeOutput);
}
int open;
using (var stream = _diskProvider.OpenReadStream(filename))
var mediaInfoModel = new MediaInfoModel
{
open = mediaInfo.Open(stream);
ContainerFormat = analysis.Format.FormatName,
VideoFormat = analysis.PrimaryVideoStream?.CodecName,
VideoCodecID = analysis.PrimaryVideoStream?.CodecTagString,
VideoProfile = analysis.PrimaryVideoStream?.Profile,
VideoBitrate = analysis.PrimaryVideoStream?.BitRate ?? 0,
VideoMultiViewCount = 1,
VideoBitDepth = GetPixelFormat(analysis.PrimaryVideoStream?.PixelFormat).Components.Min(x => x.BitDepth),
VideoColourPrimaries = analysis.PrimaryVideoStream?.ColorPrimaries,
VideoTransferCharacteristics = analysis.PrimaryVideoStream?.ColorTransfer,
DoviConfigurationRecord = analysis.PrimaryVideoStream?.SideDataList?.Find(x => x.GetType().Name == nameof(DoviConfigurationRecordSideData)) as DoviConfigurationRecordSideData,
Height = analysis.PrimaryVideoStream?.Height ?? 0,
Width = analysis.PrimaryVideoStream?.Width ?? 0,
AudioFormat = analysis.PrimaryAudioStream?.CodecName,
AudioCodecID = analysis.PrimaryAudioStream?.CodecTagString,
AudioProfile = analysis.PrimaryAudioStream?.Profile,
AudioBitrate = analysis.PrimaryAudioStream?.BitRate ?? 0,
RunTime = GetBestRuntime(analysis.PrimaryAudioStream?.Duration, analysis.PrimaryVideoStream.Duration, analysis.Format.Duration),
AudioStreamCount = analysis.AudioStreams.Count,
AudioChannels = analysis.PrimaryAudioStream?.Channels ?? 0,
AudioChannelPositions = analysis.PrimaryAudioStream?.ChannelLayout,
VideoFps = analysis.PrimaryVideoStream?.FrameRate ?? 0,
AudioLanguages = analysis.AudioStreams?.Select(x => x.Language)
.Where(l => l.IsNotNullOrWhiteSpace())
.ToList(),
Subtitles = analysis.SubtitleStreams?.Select(x => x.Language)
.Where(l => l.IsNotNullOrWhiteSpace())
.ToList(),
ScanType = "Progressive",
RawStreamData = ffprobeOutput,
SchemaRevision = CURRENT_MEDIA_INFO_SCHEMA_REVISION
};
FFProbeFrames frames = null;
// if it looks like PQ10 or similar HDR, do a frame analysis to figure out which type it is
if (PqTransferFunctions.Contains(mediaInfoModel.VideoTransferCharacteristics))
{
var frameOutput = FFProbe.GetFrameJson(filename, ffOptions: new () { ExtraArguments = "-read_intervals \"%+#1\" -select_streams v" });
mediaInfoModel.RawFrameData = frameOutput;
frames = FFProbe.AnalyseFrameJson(frameOutput);
}
if (open != 0)
{
int audioRuntime;
int videoRuntime;
int generalRuntime;
var streamSideData = analysis.PrimaryVideoStream?.SideDataList ?? new ();
var framesSideData = frames?.Frames[0]?.SideDataList ?? new ();
// Runtime
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "PlayTime"), out videoRuntime);
int.TryParse(mediaInfo.Get(StreamKind.Audio, 0, "PlayTime"), out audioRuntime);
int.TryParse(mediaInfo.Get(StreamKind.General, 0, "PlayTime"), out generalRuntime);
var sideData = streamSideData.Concat(framesSideData).ToList();
mediaInfoModel.VideoHdrFormat = GetHdrFormat(mediaInfoModel.VideoBitDepth, mediaInfoModel.VideoColourPrimaries, mediaInfoModel.VideoTransferCharacteristics, sideData);
// Audio Channels
var audioChannelsStr = mediaInfo.Get(StreamKind.Audio, 0, "Channel(s)").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
var audioChannelPositions = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions/String2");
int.TryParse(audioChannelsStr, out var audioChannels);
if (audioRuntime == 0 && videoRuntime == 0 && generalRuntime == 0)
{
// No runtime, ask mediainfo to scan the whole file
_logger.Trace("No runtime value found, rescanning at 1.0 scan depth");
mediaInfo.Option("ParseSpeed", "1.0");
using (var stream = _diskProvider.OpenReadStream(filename))
{
open = mediaInfo.Open(stream);
}
}
else if (audioChannels > 2 && audioChannelPositions.IsNullOrWhiteSpace())
{
// Some files with DTS don't have ChannelPositions unless more of the file is scanned
_logger.Trace("DTS audio without expected channel information, rescanning at 0.3 scan depth");
mediaInfo.Option("ParseSpeed", "0.3");
using (var stream = _diskProvider.OpenReadStream(filename))
{
open = mediaInfo.Open(stream);
}
}
}
if (open != 0)
{
int width;
int height;
int videoBitRate;
int audioBitRate;
int audioRuntime;
int videoRuntime;
int generalRuntime;
int streamCount;
int audioChannels;
int audioChannelsOrig;
int videoBitDepth;
decimal videoFrameRate;
int videoMultiViewCount;
string subtitles = mediaInfo.Get(StreamKind.General, 0, "Text_Language_List");
string scanType = mediaInfo.Get(StreamKind.Video, 0, "ScanType");
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "Width"), out width);
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "Height"), out height);
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitRate"), out videoBitRate);
if (videoBitRate <= 0)
{
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitRate_Nominal"), out videoBitRate);
}
decimal.TryParse(mediaInfo.Get(StreamKind.Video, 0, "FrameRate"), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out videoFrameRate);
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitDepth"), out videoBitDepth);
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "MultiView_Count"), out videoMultiViewCount);
//Runtime
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "PlayTime"), out videoRuntime);
int.TryParse(mediaInfo.Get(StreamKind.Audio, 0, "PlayTime"), out audioRuntime);
int.TryParse(mediaInfo.Get(StreamKind.General, 0, "PlayTime"), out generalRuntime);
string aBitRate = mediaInfo.Get(StreamKind.Audio, 0, "BitRate").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
int.TryParse(aBitRate, out audioBitRate);
int.TryParse(mediaInfo.Get(StreamKind.Audio, 0, "StreamCount"), out streamCount);
string audioChannelsStr = mediaInfo.Get(StreamKind.Audio, 0, "Channel(s)").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
int.TryParse(audioChannelsStr, out audioChannels);
string audioChannelsStrOrig = mediaInfo.Get(StreamKind.Audio, 0, "Channel(s)_Original").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
int.TryParse(audioChannelsStrOrig, out audioChannelsOrig);
var audioChannelPositionsText = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions");
var audioChannelPositionsTextOrig = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions_Original");
var audioChannelPositions = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions/String2");
string audioLanguages = mediaInfo.Get(StreamKind.General, 0, "Audio_Language_List");
string videoProfile = mediaInfo.Get(StreamKind.Video, 0, "Format_Profile").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
string audioProfile = mediaInfo.Get(StreamKind.Audio, 0, "Format_Profile").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
var mediaInfoModel = new MediaInfoModel
{
ContainerFormat = mediaInfo.Get(StreamKind.General, 0, "Format"),
VideoFormat = mediaInfo.Get(StreamKind.Video, 0, "Format"),
VideoCodecID = mediaInfo.Get(StreamKind.Video, 0, "CodecID"),
VideoProfile = videoProfile,
VideoCodecLibrary = mediaInfo.Get(StreamKind.Video, 0, "Encoded_Library"),
VideoBitrate = videoBitRate,
VideoBitDepth = videoBitDepth,
VideoMultiViewCount = videoMultiViewCount,
VideoColourPrimaries = mediaInfo.Get(StreamKind.Video, 0, "colour_primaries"),
VideoTransferCharacteristics = mediaInfo.Get(StreamKind.Video, 0, "transfer_characteristics"),
VideoHdrFormat = mediaInfo.Get(StreamKind.Video, 0, "HDR_Format"),
VideoHdrFormatCompatibility = mediaInfo.Get(StreamKind.Video, 0, "HDR_Format_Compatibility"),
Height = height,
Width = width,
AudioFormat = mediaInfo.Get(StreamKind.Audio, 0, "Format"),
AudioCodecID = mediaInfo.Get(StreamKind.Audio, 0, "CodecID"),
AudioProfile = audioProfile,
AudioCodecLibrary = mediaInfo.Get(StreamKind.Audio, 0, "Encoded_Library"),
AudioAdditionalFeatures = mediaInfo.Get(StreamKind.Audio, 0, "Format_AdditionalFeatures"),
AudioBitrate = audioBitRate,
RunTime = GetBestRuntime(audioRuntime, videoRuntime, generalRuntime),
AudioStreamCount = streamCount,
AudioChannelsContainer = audioChannels,
AudioChannelsStream = audioChannelsOrig,
AudioChannelPositions = audioChannelPositions,
AudioChannelPositionsTextContainer = audioChannelPositionsText,
AudioChannelPositionsTextStream = audioChannelPositionsTextOrig,
VideoFps = videoFrameRate,
AudioLanguages = audioLanguages,
Subtitles = subtitles,
ScanType = scanType,
SchemaRevision = CURRENT_MEDIA_INFO_SCHEMA_REVISION
};
return mediaInfoModel;
}
else
{
_logger.Warn("Unable to open media info from file: " + filename);
}
}
catch (DllNotFoundException ex)
{
_logger.Error(ex, "mediainfo is required but was not found");
return mediaInfoModel;
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to parse media info from file: {0}", filename);
}
finally
{
mediaInfo?.Close();
}
return null;
}
@@ -220,19 +137,70 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
return info?.RunTime;
}
private TimeSpan GetBestRuntime(int audio, int video, int general)
private static TimeSpan GetBestRuntime(TimeSpan? audio, TimeSpan? video, TimeSpan general)
{
if (video == 0)
if (!video.HasValue || video.Value.TotalMilliseconds == 0)
{
if (audio == 0)
if (!audio.HasValue || audio.Value.TotalMilliseconds == 0)
{
return TimeSpan.FromMilliseconds(general);
return general;
}
return TimeSpan.FromMilliseconds(audio);
return audio.Value;
}
return TimeSpan.FromMilliseconds(video);
return video.Value;
}
private FFProbePixelFormat GetPixelFormat(string format)
{
return _pixelFormats.Find(x => x.Name == format);
}
public static HdrFormat GetHdrFormat(int bitDepth, string colorPrimaries, string transferFunction, List<SideData> sideData)
{
if (bitDepth < 10)
{
return HdrFormat.None;
}
if (TryFindSideData(sideData, nameof(DoviConfigurationRecordSideData)))
{
return HdrFormat.DolbyVision;
}
if (!ValidHdrColourPrimaries.Contains(colorPrimaries) || !ValidHdrTransferFunctions.Contains(transferFunction))
{
return HdrFormat.None;
}
if (HlgTransferFunctions.Contains(transferFunction))
{
return HdrFormat.Hlg10;
}
if (PqTransferFunctions.Contains(transferFunction))
{
if (TryFindSideData(sideData, nameof(HdrDynamicMetadataSpmte2094)))
{
return HdrFormat.Hdr10Plus;
}
if (TryFindSideData(sideData, nameof(MasteringDisplayMetadata)) ||
TryFindSideData(sideData, nameof(ContentLightLevelMetadata)))
{
return HdrFormat.Hdr10;
}
return HdrFormat.Pq10;
}
return HdrFormat.None;
}
private static bool TryFindSideData(List<SideData> list, string typeName)
{
return list?.Find(x => x.GetType().Name == typeName) != null;
}
}
}
@@ -19,13 +19,13 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Aggregation.Aggregators.Augmenter
return null;
}
var audioLanguages = localMovie.MediaInfo.AudioLanguages.Split('/').Select(l => l.Trim()).Distinct().ToList();
var audioLanguages = localMovie.MediaInfo.AudioLanguages.Distinct().ToList();
var languages = new List<Languages.Language>();
foreach (var audioLanguage in audioLanguages)
{
var language = IsoLanguages.FindByName(audioLanguage)?.Language;
var language = IsoLanguages.Find(audioLanguage)?.Language;
languages.AddIfNotNull(language);
}
@@ -187,11 +187,11 @@ namespace NzbDrone.Core.Notifications.Discord
break;
case DiscordImportFieldType.Languages:
discordField.Name = "Languages";
discordField.Value = message.MovieFile.MediaInfo.AudioLanguages;
discordField.Value = message.MovieFile.MediaInfo.AudioLanguages.ConcatToString("/");
break;
case DiscordImportFieldType.Subtitles:
discordField.Name = "Subtitles";
discordField.Value = message.MovieFile.MediaInfo.Subtitles;
discordField.Value = message.MovieFile.MediaInfo.Subtitles.ConcatToString("/");
break;
case DiscordImportFieldType.Release:
discordField.Name = "Release";
+39 -26
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -69,6 +70,31 @@ namespace NzbDrone.Core.Organizer
private static readonly Regex ReservedDeviceNamesRegex = new Regex(@"^(?:aux|com[1-9]|con|lpt[1-9]|nul|prn)\.", RegexOptions.Compiled | RegexOptions.IgnoreCase);
// generated from https://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
public static readonly ImmutableDictionary<string, string> Iso639BTMap = new Dictionary<string, string>
{
{ "alb", "sqi" },
{ "arm", "hye" },
{ "baq", "eus" },
{ "bur", "mya" },
{ "chi", "zho" },
{ "cze", "ces" },
{ "dut", "nld" },
{ "fre", "fra" },
{ "geo", "kat" },
{ "ger", "deu" },
{ "gre", "ell" },
{ "ice", "isl" },
{ "mac", "mkd" },
{ "mao", "mri" },
{ "may", "msa" },
{ "per", "fas" },
{ "rum", "ron" },
{ "slo", "slk" },
{ "tib", "bod" },
{ "wel", "cym" }
}.ToImmutableDictionary();
public FileNameBuilder(INamingConfigService namingConfigService,
IQualityDefinitionService qualityDefinitionService,
IUpdateMediaInfo mediaInfoUpdater,
@@ -344,8 +370,8 @@ namespace NzbDrone.Core.Organizer
var videoCodec = MediaInfoFormatter.FormatVideoCodec(movieFile.MediaInfo, sceneName);
var audioCodec = MediaInfoFormatter.FormatAudioCodec(movieFile.MediaInfo, sceneName);
var audioChannels = MediaInfoFormatter.FormatAudioChannels(movieFile.MediaInfo);
var audioLanguages = movieFile.MediaInfo.AudioLanguages ?? string.Empty;
var subtitles = movieFile.MediaInfo.Subtitles ?? string.Empty;
var audioLanguages = movieFile.MediaInfo.AudioLanguages ?? new List<string>();
var subtitles = movieFile.MediaInfo.Subtitles ?? new List<string>();
var mediaInfoAudioLanguages = GetLanguagesToken(audioLanguages);
if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace())
@@ -365,7 +391,7 @@ namespace NzbDrone.Core.Organizer
mediaInfoSubtitleLanguages = $"[{mediaInfoSubtitleLanguages}]";
}
var videoBitDepth = movieFile.MediaInfo.VideoBitDepth > 0 ? movieFile.MediaInfo.VideoBitDepth.ToString() : string.Empty;
var videoBitDepth = movieFile.MediaInfo.VideoBitDepth > 0 ? movieFile.MediaInfo.VideoBitDepth.ToString() : 8.ToString();
var audioChannelsFormatted = audioChannels > 0 ?
audioChannels.ToString("F1", CultureInfo.InvariantCulture) :
string.Empty;
@@ -405,42 +431,29 @@ namespace NzbDrone.Core.Organizer
tokenHandlers["{Custom Formats}"] = m => string.Join(" ", customFormats.Where(x => x.IncludeCustomFormatWhenRenaming));
}
private string GetLanguagesToken(string mediaInfoLanguages)
private string GetLanguagesToken(List<string> mediaInfoLanguages)
{
List<string> tokens = new List<string>();
foreach (var item in mediaInfoLanguages.Split('/'))
var tokens = new List<string>();
foreach (var item in mediaInfoLanguages)
{
if (!string.IsNullOrWhiteSpace(item))
if (!string.IsNullOrWhiteSpace(item) && item != "und")
{
tokens.Add(item.Trim());
}
}
var cultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
for (int i = 0; i < tokens.Count; i++)
{
if (tokens[i] == "Swedis")
{
// Probably typo in mediainfo (should be 'Swedish')
tokens[i] = "SV";
continue;
}
if (tokens[i] == "Chinese" && OsInfo.IsNotWindows)
{
// Mono only has 'Chinese (Simplified)' & 'Chinese (Traditional)'
tokens[i] = "ZH";
continue;
}
try
{
var cultureInfo = cultures.FirstOrDefault(p => p.EnglishName.RemoveAccent() == tokens[i]);
if (cultureInfo != null)
var token = tokens[i].ToLowerInvariant();
if (Iso639BTMap.TryGetValue(token, out var mapped))
{
tokens[i] = cultureInfo.TwoLetterISOLanguageName.ToUpper();
token = mapped;
}
var cultureInfo = new CultureInfo(token);
tokens[i] = cultureInfo.TwoLetterISOLanguageName.ToUpper();
}
catch
{
@@ -30,13 +30,13 @@ namespace NzbDrone.Core.Organizer
VideoFormat = "AVC",
VideoBitDepth = 10,
VideoMultiViewCount = 2,
VideoColourPrimaries = "BT.2020",
VideoColourPrimaries = "bt2020",
VideoTransferCharacteristics = "HLG",
AudioFormat = "DTS",
AudioChannelsContainer = 6,
AudioChannelPositions = "3/2/0.1",
AudioLanguages = "German",
Subtitles = "English/German"
AudioChannels = 6,
AudioChannelPositions = "5.1",
AudioLanguages = new List<string> { "ger" },
Subtitles = new List<string> { "eng", "ger" }
};
_movieFile = new MovieFile
+7
View File
@@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices.ComTypes;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Organizer;
namespace NzbDrone.Core.Parser
{
@@ -62,6 +64,11 @@ namespace NzbDrone.Core.Parser
else if (langCode.Length == 3)
{
//Lookup ISO639-2T code
if (FileNameBuilder.Iso639BTMap.TryGetValue(langCode, out var mapped))
{
langCode = mapped;
}
return All.FirstOrDefault(l => l.ThreeLetterCode == langCode);
}
+2
View File
@@ -5,6 +5,7 @@
<ItemGroup>
<PackageReference Include="Dapper" Version="2.0.90" />
<PackageReference Include="MailKit" Version="2.15.0" />
<PackageReference Include="Servarr.FFprobe" Version="4.4.1.41" />
<PackageReference Include="System.Memory" Version="4.5.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
@@ -19,6 +20,7 @@
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />
<PackageReference Include="System.Text.Json" Version="5.0.2" />
<PackageReference Include="MonoTorrent" Version="1.0.29" />
<PackageReference Include="FFMpegCore" Version="4.6.16" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Common\Radarr.Common.csproj" />