/// <summary> /// Normalizes the FF probe result. /// </summary> /// <param name="result">The result.</param> public static void NormalizeFFProbeResult(InternalMediaInfoResult result) { if (result == null) { throw new ArgumentNullException("result"); } if (result.format != null && result.format.tags != null) { result.format.tags = ConvertDictionaryToCaseInSensitive(result.format.tags); } if (result.streams != null) { // Convert all dictionaries to case insensitive foreach (var stream in result.streams) { if (stream.tags != null) { stream.tags = ConvertDictionaryToCaseInSensitive(stream.tags); } if (stream.disposition != null) { stream.disposition = ConvertDictionaryToCaseInSensitive(stream.disposition); } } } }
/// <summary> /// Normalizes the FF probe result. /// </summary> /// <param name="result">The result.</param> public static void NormalizeFFProbeResult(InternalMediaInfoResult result) { if (result == null) { throw new ArgumentNullException(nameof(result)); } if (result.format != null && result.format.tags != null) { result.format.tags = ConvertDictionaryToCaseInSensitive(result.format.tags); } if (result.streams != null) { // Convert all dictionaries to case insensitive foreach (var stream in result.streams) { if (stream.tags != null) { stream.tags = ConvertDictionaryToCaseInSensitive(stream.tags); } if (stream.disposition != null) { stream.disposition = ConvertDictionaryToCaseInSensitive(stream.disposition); } } } }
private void SetAudioRuntimeTicks(InternalMediaInfoResult result, Model.MediaInfo.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.MediaInfo.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 const int MaxSubtitleDescriptionExtractionLength = 100; // When extracting subtitles, the maximum length to consider (to avoid invalid filenames) private void FetchWtvInfo(Model.MediaInfo.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; } }
public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType videoType, bool isAudio, string path, MediaProtocol protocol) { var info = new Model.MediaInfo.MediaInfo { Path = path, Protocol = protocol }; FFProbeHelpers.NormalizeFFProbeResult(data); SetSize(data, info); var internalStreams = data.streams ?? new MediaStreamInfo[] { }; info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, 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)) { int value; if (int.TryParse(data.format.bit_rate, NumberStyles.Any, _usCulture, out value)) { info.Bitrate = value; } } } if (isAudio) { SetAudioRuntimeTicks(data, info); var tags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); // tags are normally located under data.format, but we've seen some cases with ogg where they're part of the audio stream // so let's create a combined list of both if (data.streams != null) { var audioStream = data.streams.FirstOrDefault(i => string.Equals(i.codec_type, "audio", StringComparison.OrdinalIgnoreCase)); if (audioStream != null && audioStream.tags != null) { foreach (var pair in audioStream.tags) { tags[pair.Key] = pair.Value; } } } if (data.format != null && data.format.tags != null) { foreach (var pair in data.format.tags) { tags[pair.Key] = pair.Value; } } SetAudioInfoFromTags(info, tags); } else { if (data.format != null && !string.IsNullOrEmpty(data.format.duration)) { info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration, _usCulture)).Ticks; } FetchWtvInfo(info, data); if (data.Chapters != null) { info.Chapters = data.Chapters.Select(GetChapterInfo).ToList(); } ExtractTimestamp(info); } return info; }
public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType videoType, bool isAudio, string path, MediaProtocol protocol) { var info = new MediaInfo { Path = path, Protocol = protocol }; FFProbeHelpers.NormalizeFFProbeResult(data); SetSize(data, info); var internalStreams = data.streams ?? new MediaStreamInfo[] { }; info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, 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)) { int value; if (int.TryParse(data.format.bit_rate, NumberStyles.Any, _usCulture, out value)) { info.Bitrate = value; } } } var tags = new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase); var tagStreamType = isAudio ? "audio" : "video"; if (data.streams != null) { var tagStream = data.streams.FirstOrDefault(i => string.Equals(i.codec_type, tagStreamType, StringComparison.OrdinalIgnoreCase)); if (tagStream != null && tagStream.tags != null) { foreach (var pair in tagStream.tags) { tags[pair.Key] = pair.Value; } } } if (data.format != null && data.format.tags != null) { foreach (var pair in data.format.tags) { tags[pair.Key] = pair.Value; } } FetchGenres(info, tags); var shortOverview = FFProbeHelpers.GetDictionaryValue(tags, "description"); var overview = FFProbeHelpers.GetDictionaryValue(tags, "synopsis"); if (string.IsNullOrWhiteSpace(overview)) { overview = shortOverview; shortOverview = null; } if (string.IsNullOrWhiteSpace(overview)) { overview = FFProbeHelpers.GetDictionaryValue(tags, "desc"); } if (!string.IsNullOrWhiteSpace(overview)) { info.Overview = overview; } if (!string.IsNullOrWhiteSpace(shortOverview)) { info.ShortOverview = shortOverview; } var title = FFProbeHelpers.GetDictionaryValue(tags, "title"); if (!string.IsNullOrWhiteSpace(title)) { info.Name = title; } info.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date"); // Several different forms of retaildate info.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "date"); if (isAudio) { SetAudioRuntimeTicks(data, info); // tags are normally located under data.format, but we've seen some cases with ogg where they're part of the info stream // so let's create a combined list of both SetAudioInfoFromTags(info, tags); } else { FetchStudios(info, tags, "copyright"); var iTunEXTC = FFProbeHelpers.GetDictionaryValue(tags, "iTunEXTC"); if (!string.IsNullOrWhiteSpace(iTunEXTC)) { var parts = iTunEXTC.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); // Example // mpaa|G|100|For crude humor if (parts.Length > 1) { info.OfficialRating = parts[1]; if (parts.Length > 3) { info.OfficialRatingDescription = parts[3]; } } } var itunesXml = FFProbeHelpers.GetDictionaryValue(tags, "iTunMOVI"); if (!string.IsNullOrWhiteSpace(itunesXml)) { FetchFromItunesInfo(itunesXml, info); } if (data.format != null && !string.IsNullOrEmpty(data.format.duration)) { info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration, _usCulture)).Ticks; } FetchWtvInfo(info, data); if (data.Chapters != null) { info.Chapters = data.Chapters.Select(GetChapterInfo).ToList(); } ExtractTimestamp(info); } return(info); }
private const int MaxSubtitleDescriptionExtractionLength = 100; // When extracting subtitles, the maximum length to consider (to avoid invalid filenames) private void FetchWtvInfo(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)) { var genreList = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries) .Where(i => !string.IsNullOrWhiteSpace(i)) .Select(i => i.Trim()) .ToList(); // If this is empty then don't overwrite genres that might have been fetched earlier if (genreList.Count > 0) { video.Genres = genreList; } } 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; } }
public Model.MediaInfo.MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType videoType, bool isAudio, string path, MediaProtocol protocol) { var info = new Model.MediaInfo.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 { if (data.format != null && !string.IsNullOrEmpty(data.format.duration)) { info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration, _usCulture)).Ticks; } 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 && videoType == VideoType.VideoFile) { UpdateFromMediaInfo(info, videoStream); } } return(info); }
public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType videoType, bool isAudio, string path, MediaProtocol protocol) { var info = new MediaInfo { Path = path, Protocol = protocol }; FFProbeHelpers.NormalizeFFProbeResult(data); SetSize(data, info); var internalStreams = data.streams ?? new MediaStreamInfo[] { }; info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, 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)) { int value; if (int.TryParse(data.format.bit_rate, NumberStyles.Any, _usCulture, out value)) { info.Bitrate = value; } } } var tags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var tagStreamType = isAudio ? "audio" : "video"; if (data.streams != null) { var tagStream = data.streams.FirstOrDefault(i => string.Equals(i.codec_type, tagStreamType, StringComparison.OrdinalIgnoreCase)); if (tagStream != null && tagStream.tags != null) { foreach (var pair in tagStream.tags) { tags[pair.Key] = pair.Value; } } } if (data.format != null && data.format.tags != null) { foreach (var pair in data.format.tags) { tags[pair.Key] = pair.Value; } } FetchGenres(info, tags); var shortOverview = FFProbeHelpers.GetDictionaryValue(tags, "description"); var overview = FFProbeHelpers.GetDictionaryValue(tags, "synopsis"); if (string.IsNullOrWhiteSpace(overview)) { overview = shortOverview; shortOverview = null; } if (string.IsNullOrWhiteSpace(overview)) { overview = FFProbeHelpers.GetDictionaryValue(tags, "desc"); } if (!string.IsNullOrWhiteSpace(overview)) { info.Overview = overview; } if (!string.IsNullOrWhiteSpace(shortOverview)) { info.ShortOverview = shortOverview; } var title = FFProbeHelpers.GetDictionaryValue(tags, "title"); if (!string.IsNullOrWhiteSpace(title)) { info.Name = title; } info.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date"); // Several different forms of retaildate info.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "date"); if (isAudio) { SetAudioRuntimeTicks(data, info); // tags are normally located under data.format, but we've seen some cases with ogg where they're part of the info stream // so let's create a combined list of both SetAudioInfoFromTags(info, tags); } else { FetchStudios(info, tags, "copyright"); var iTunEXTC = FFProbeHelpers.GetDictionaryValue(tags, "iTunEXTC"); if (!string.IsNullOrWhiteSpace(iTunEXTC)) { var parts = iTunEXTC.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); // Example // mpaa|G|100|For crude humor if (parts.Length > 1) { info.OfficialRating = parts[1]; if (parts.Length > 3) { info.OfficialRatingDescription = parts[3]; } } } var itunesXml = FFProbeHelpers.GetDictionaryValue(tags, "iTunMOVI"); if (!string.IsNullOrWhiteSpace(itunesXml)) { FetchFromItunesInfo(itunesXml, info); } if (data.format != null && !string.IsNullOrEmpty(data.format.duration)) { info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration, _usCulture)).Ticks; } FetchWtvInfo(info, data); if (data.Chapters != null) { info.Chapters = data.Chapters.Select(GetChapterInfo).ToList(); } ExtractTimestamp(info); var stereoMode = GetDictionaryValue(tags, "stereo_mode"); if (string.Equals(stereoMode, "left_right", StringComparison.OrdinalIgnoreCase)) { info.Video3DFormat = Video3DFormat.FullSideBySide; } } return info; }
public Model.MediaInfo.MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType videoType, bool isAudio, string path, MediaProtocol protocol) { var info = new Model.MediaInfo.MediaInfo { Path = path, Protocol = protocol }; FFProbeHelpers.NormalizeFFProbeResult(data); SetSize(data, info); var internalStreams = data.streams ?? new MediaStreamInfo[] { }; info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, 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); var tags = new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase); // tags are normally located under data.format, but we've seen some cases with ogg where they're part of the audio stream // so let's create a combined list of both if (data.streams != null) { var audioStream = data.streams.FirstOrDefault(i => string.Equals(i.codec_type, "audio", StringComparison.OrdinalIgnoreCase)); if (audioStream != null && audioStream.tags != null) { foreach (var pair in audioStream.tags) { tags[pair.Key] = pair.Value; } } } if (data.format != null && data.format.tags != null) { foreach (var pair in data.format.tags) { tags[pair.Key] = pair.Value; } } SetAudioInfoFromTags(info, tags); } else { if (data.format != null && !string.IsNullOrEmpty(data.format.duration)) { info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration, _usCulture)).Ticks; } 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 && videoType == VideoType.VideoFile) { UpdateFromMediaInfo(info, videoStream); } } return(info); }