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("SELECT \"Id\", \"MediaInfo\", \"SceneName\" FROM \"MovieFiles\""); var updated = new List(); 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(row.MediaInfo, _serializerSettings); if (mediaInfoVersion.SchemaRevision >= 8) { continue; } // parse and migrate var mediaInfo = JsonSerializer.Deserialize(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 MigrateLanguages(string mediaInfoLanguages) { var languages = new List(); 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.IsNotNullOrWhiteSpace() ? primary.Replace("BT.", "bt") : primary; } 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 AudioLanguages { get; set; } public List Subtitles { get; set; } public string ScanType { get; set; } } } }