From 2b7a80cfb5b9212260734c095a5b3439af7d64e2 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sat, 4 Apr 2015 15:35:29 -0400 Subject: improve direct play of live streams --- .../Probing/ProbeResultNormalizer.cs | 882 +++++++++++++++++++++ 1 file changed, 882 insertions(+) create mode 100644 MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs (limited to 'MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs') diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs new file mode 100644 index 000000000..48e8b6ee2 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -0,0 +1,882 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.MediaInfo; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; + +namespace MediaBrowser.MediaEncoding.Probing +{ + public class ProbeResultNormalizer + { + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + + public ProbeResultNormalizer(ILogger logger, IFileSystem fileSystem) + { + _logger = logger; + _fileSystem = fileSystem; + } + + public Model.Entities.MediaInfo GetMediaInfo(InternalMediaInfoResult data, bool isAudio, string path, MediaProtocol protocol) + { + var info = new Model.Entities.MediaInfo + { + Path = path, + Protocol = protocol + }; + + FFProbeHelpers.NormalizeFFProbeResult(data); + SetSize(data, info); + + var internalStreams = data.streams ?? new MediaStreamInfo[] { }; + + info.MediaStreams = internalStreams.Select(s => GetMediaStream(s, data.format)) + .Where(i => i != null) + .ToList(); + + if (data.format != null) + { + info.Container = data.format.format_name; + + if (!string.IsNullOrEmpty(data.format.bit_rate)) + { + info.Bitrate = int.Parse(data.format.bit_rate, _usCulture); + } + } + + if (isAudio) + { + SetAudioRuntimeTicks(data, info); + + if (data.format != null && data.format.tags != null) + { + SetAudioInfoFromTags(info, data.format.tags); + } + } + else + { + FetchWtvInfo(info, data); + + if (data.Chapters != null) + { + info.Chapters = data.Chapters.Select(GetChapterInfo).ToList(); + } + + ExtractTimestamp(info); + + var videoStream = info.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video); + + if (videoStream != null) + { + UpdateFromMediaInfo(info, videoStream); + } + } + + return info; + } + + /// + /// Converts ffprobe stream info to our MediaStream class + /// + /// The stream info. + /// The format info. + /// MediaStream. + private MediaStream GetMediaStream(MediaStreamInfo streamInfo, MediaFormatInfo formatInfo) + { + var stream = new MediaStream + { + Codec = streamInfo.codec_name, + Profile = streamInfo.profile, + Level = streamInfo.level, + Index = streamInfo.index, + PixelFormat = streamInfo.pix_fmt + }; + + if (streamInfo.tags != null) + { + stream.Language = GetDictionaryValue(streamInfo.tags, "language"); + } + + if (string.Equals(streamInfo.codec_type, "audio", StringComparison.OrdinalIgnoreCase)) + { + stream.Type = MediaStreamType.Audio; + + stream.Channels = streamInfo.channels; + + if (!string.IsNullOrEmpty(streamInfo.sample_rate)) + { + stream.SampleRate = int.Parse(streamInfo.sample_rate, _usCulture); + } + + stream.ChannelLayout = ParseChannelLayout(streamInfo.channel_layout); + } + else if (string.Equals(streamInfo.codec_type, "subtitle", StringComparison.OrdinalIgnoreCase)) + { + stream.Type = MediaStreamType.Subtitle; + } + else if (string.Equals(streamInfo.codec_type, "video", StringComparison.OrdinalIgnoreCase)) + { + stream.Type = (streamInfo.codec_name ?? string.Empty).IndexOf("mjpeg", StringComparison.OrdinalIgnoreCase) != -1 + ? MediaStreamType.EmbeddedImage + : MediaStreamType.Video; + + stream.Width = streamInfo.width; + stream.Height = streamInfo.height; + stream.AspectRatio = GetAspectRatio(streamInfo); + + stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate); + stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate); + + stream.BitDepth = GetBitDepth(stream.PixelFormat); + + //stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) || + // string.Equals(stream.AspectRatio, "2.35:1", StringComparison.OrdinalIgnoreCase) || + // string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase); + + stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase); + } + else + { + return null; + } + + // Get stream bitrate + var bitrate = 0; + + if (!string.IsNullOrEmpty(streamInfo.bit_rate)) + { + bitrate = int.Parse(streamInfo.bit_rate, _usCulture); + } + else if (formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate) && stream.Type == MediaStreamType.Video) + { + // If the stream info doesn't have a bitrate get the value from the media format info + bitrate = int.Parse(formatInfo.bit_rate, _usCulture); + } + + if (bitrate > 0) + { + stream.BitRate = bitrate; + } + + if (streamInfo.disposition != null) + { + var isDefault = GetDictionaryValue(streamInfo.disposition, "default"); + var isForced = GetDictionaryValue(streamInfo.disposition, "forced"); + + stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase); + + stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase); + } + + return stream; + } + + private int? GetBitDepth(string pixelFormat) + { + var eightBit = new List + { + "yuv420p", + "yuv411p", + "yuvj420p", + "uyyvyy411", + "nv12", + "nv21", + "rgb444le", + "rgb444be", + "bgr444le", + "bgr444be", + "yuvj411p" + }; + + if (!string.IsNullOrEmpty(pixelFormat)) + { + if (eightBit.Contains(pixelFormat, StringComparer.OrdinalIgnoreCase)) + { + return 8; + } + } + + return null; + } + + /// + /// Gets a string from an FFProbeResult tags dictionary + /// + /// The tags. + /// The key. + /// System.String. + private string GetDictionaryValue(Dictionary tags, string key) + { + if (tags == null) + { + return null; + } + + string val; + + tags.TryGetValue(key, out val); + return val; + } + + private string ParseChannelLayout(string input) + { + if (string.IsNullOrEmpty(input)) + { + return input; + } + + return input.Split('(').FirstOrDefault(); + } + + private string GetAspectRatio(MediaStreamInfo info) + { + var original = info.display_aspect_ratio; + + int height; + int width; + + var parts = (original ?? string.Empty).Split(':'); + if (!(parts.Length == 2 && + int.TryParse(parts[0], NumberStyles.Any, _usCulture, out width) && + int.TryParse(parts[1], NumberStyles.Any, _usCulture, out height) && + width > 0 && + height > 0)) + { + width = info.width; + height = info.height; + } + + if (width > 0 && height > 0) + { + double ratio = width; + ratio /= height; + + if (IsClose(ratio, 1.777777778, .03)) + { + return "16:9"; + } + + if (IsClose(ratio, 1.3333333333, .05)) + { + return "4:3"; + } + + if (IsClose(ratio, 1.41)) + { + return "1.41:1"; + } + + if (IsClose(ratio, 1.5)) + { + return "1.5:1"; + } + + if (IsClose(ratio, 1.6)) + { + return "1.6:1"; + } + + if (IsClose(ratio, 1.66666666667)) + { + return "5:3"; + } + + if (IsClose(ratio, 1.85, .02)) + { + return "1.85:1"; + } + + if (IsClose(ratio, 2.35, .025)) + { + return "2.35:1"; + } + + if (IsClose(ratio, 2.4, .025)) + { + return "2.40:1"; + } + } + + return original; + } + + private bool IsClose(double d1, double d2, double variance = .005) + { + return Math.Abs(d1 - d2) <= variance; + } + + /// + /// Gets a frame rate from a string value in ffprobe output + /// This could be a number or in the format of 2997/125. + /// + /// The value. + /// System.Nullable{System.Single}. + private float? GetFrameRate(string value) + { + if (!string.IsNullOrEmpty(value)) + { + var parts = value.Split('/'); + + float result; + + if (parts.Length == 2) + { + result = float.Parse(parts[0], _usCulture) / float.Parse(parts[1], _usCulture); + } + else + { + result = float.Parse(parts[0], _usCulture); + } + + return float.IsNaN(result) ? (float?)null : result; + } + + return null; + } + + private void SetAudioRuntimeTicks(InternalMediaInfoResult result, Model.Entities.MediaInfo data) + { + if (result.streams != null) + { + // Get the first audio stream + var stream = result.streams.FirstOrDefault(s => string.Equals(s.codec_type, "audio", StringComparison.OrdinalIgnoreCase)); + + if (stream != null) + { + // Get duration from stream properties + var duration = stream.duration; + + // If it's not there go into format properties + if (string.IsNullOrEmpty(duration)) + { + duration = result.format.duration; + } + + // If we got something, parse it + if (!string.IsNullOrEmpty(duration)) + { + data.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration, _usCulture)).Ticks; + } + } + } + } + + private void SetSize(InternalMediaInfoResult data, Model.Entities.MediaInfo info) + { + if (data.format != null) + { + if (!string.IsNullOrEmpty(data.format.size)) + { + info.Size = long.Parse(data.format.size, _usCulture); + } + else + { + info.Size = null; + } + } + } + + private void SetAudioInfoFromTags(Model.Entities.MediaInfo audio, Dictionary tags) + { + var title = FFProbeHelpers.GetDictionaryValue(tags, "title"); + + // Only set Name if title was found in the dictionary + if (!string.IsNullOrEmpty(title)) + { + audio.Title = title; + } + + var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer"); + + if (!string.IsNullOrWhiteSpace(composer)) + { + foreach (var person in Split(composer, false)) + { + audio.People.Add(new BaseItemPerson { Name = person, Type = PersonType.Composer }); + } + } + + audio.Album = FFProbeHelpers.GetDictionaryValue(tags, "album"); + + var artists = FFProbeHelpers.GetDictionaryValue(tags, "artists"); + + if (!string.IsNullOrWhiteSpace(artists)) + { + audio.Artists = artists.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + } + else + { + var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist"); + if (string.IsNullOrWhiteSpace(artist)) + { + audio.Artists.Clear(); + } + else + { + audio.Artists = SplitArtists(artist) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + } + } + + var albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "albumartist"); + if (string.IsNullOrWhiteSpace(albumArtist)) + { + albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "album artist"); + } + if (string.IsNullOrWhiteSpace(albumArtist)) + { + albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "album_artist"); + } + + if (string.IsNullOrWhiteSpace(albumArtist)) + { + audio.AlbumArtists = new List(); + } + else + { + audio.AlbumArtists = SplitArtists(albumArtist) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + } + + // Track number + audio.IndexNumber = GetDictionaryDiscValue(tags, "track"); + + // Disc number + audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc"); + + audio.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date"); + + // Several different forms of retaildate + audio.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ?? + FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ?? + FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ?? + FFProbeHelpers.GetDictionaryDateTime(tags, "date"); + + // If we don't have a ProductionYear try and get it from PremiereDate + if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue) + { + audio.ProductionYear = audio.PremiereDate.Value.ToLocalTime().Year; + } + + FetchGenres(audio, tags); + + // There's several values in tags may or may not be present + FetchStudios(audio, tags, "organization"); + FetchStudios(audio, tags, "ensemble"); + FetchStudios(audio, tags, "publisher"); + + // These support mulitple values, but for now we only store the first. + audio.SetProviderId(MetadataProviders.MusicBrainzAlbumArtist, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Artist Id"))); + audio.SetProviderId(MetadataProviders.MusicBrainzArtist, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Artist Id"))); + + audio.SetProviderId(MetadataProviders.MusicBrainzAlbum, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Id"))); + audio.SetProviderId(MetadataProviders.MusicBrainzReleaseGroup, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Group Id"))); + audio.SetProviderId(MetadataProviders.MusicBrainzTrack, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Track Id"))); + } + + private string GetMultipleMusicBrainzId(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + return value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries) + .Select(i => i.Trim()) + .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i)); + } + + private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' }; + + /// + /// Splits the specified val. + /// + /// The val. + /// if set to true [allow comma delimiter]. + /// System.String[][]. + private IEnumerable Split(string val, bool allowCommaDelimiter) + { + // Only use the comma as a delimeter if there are no slashes or pipes. + // We want to be careful not to split names that have commas in them + var delimeter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.IndexOf(i) != -1) ? + _nameDelimiters : + new[] { ',' }; + + return val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries) + .Where(i => !string.IsNullOrWhiteSpace(i)) + .Select(i => i.Trim()); + } + + private const string ArtistReplaceValue = " | "; + + private IEnumerable SplitArtists(string val) + { + val = val.Replace(" featuring ", ArtistReplaceValue, StringComparison.OrdinalIgnoreCase) + .Replace(" feat. ", ArtistReplaceValue, StringComparison.OrdinalIgnoreCase); + + var artistsFound = new List(); + + foreach (var whitelistArtist in GetSplitWhitelist()) + { + var originalVal = val; + val = val.Replace(whitelistArtist, "|", StringComparison.OrdinalIgnoreCase); + + if (!string.Equals(originalVal, val, StringComparison.OrdinalIgnoreCase)) + { + artistsFound.Add(whitelistArtist); + } + } + + // Only use the comma as a delimeter if there are no slashes or pipes. + // We want to be careful not to split names that have commas in them + var delimeter = _nameDelimiters; + + var artists = val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries) + .Where(i => !string.IsNullOrWhiteSpace(i)) + .Select(i => i.Trim()); + + artistsFound.AddRange(artists); + return artistsFound; + } + + + private List _splitWhiteList = null; + + private IEnumerable GetSplitWhitelist() + { + if (_splitWhiteList == null) + { + var file = GetType().Namespace + ".whitelist.txt"; + + using (var stream = GetType().Assembly.GetManifestResourceStream(file)) + { + using (var reader = new StreamReader(stream)) + { + var list = new List(); + + while (!reader.EndOfStream) + { + var val = reader.ReadLine(); + + if (!string.IsNullOrWhiteSpace(val)) + { + list.Add(val); + } + } + + _splitWhiteList = list; + } + } + } + + return _splitWhiteList; + } + + /// + /// Gets the studios from the tags collection + /// + /// The audio. + /// The tags. + /// Name of the tag. + private void FetchStudios(Model.Entities.MediaInfo audio, Dictionary tags, string tagName) + { + var val = FFProbeHelpers.GetDictionaryValue(tags, tagName); + + if (!string.IsNullOrEmpty(val)) + { + var studios = Split(val, true); + + foreach (var studio in studios) + { + // Sometimes the artist name is listed here, account for that + if (audio.Artists.Contains(studio, StringComparer.OrdinalIgnoreCase)) + { + continue; + } + if (audio.AlbumArtists.Contains(studio, StringComparer.OrdinalIgnoreCase)) + { + continue; + } + + audio.Studios.Add(studio); + } + + audio.Studios = audio.Studios + .Where(i => !string.IsNullOrWhiteSpace(i)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + } + } + + /// + /// Gets the genres from the tags collection + /// + /// The information. + /// The tags. + private void FetchGenres(Model.Entities.MediaInfo info, Dictionary tags) + { + var val = FFProbeHelpers.GetDictionaryValue(tags, "genre"); + + if (!string.IsNullOrEmpty(val)) + { + foreach (var genre in Split(val, true)) + { + info.Genres.Add(genre); + } + + info.Genres = info.Genres + .Where(i => !string.IsNullOrWhiteSpace(i)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + } + } + + /// + /// Gets the disc number, which is sometimes can be in the form of '1', or '1/3' + /// + /// The tags. + /// Name of the tag. + /// System.Nullable{System.Int32}. + private int? GetDictionaryDiscValue(Dictionary tags, string tagName) + { + var disc = FFProbeHelpers.GetDictionaryValue(tags, tagName); + + if (!string.IsNullOrEmpty(disc)) + { + disc = disc.Split('/')[0]; + + int num; + + if (int.TryParse(disc, out num)) + { + return num; + } + } + + return null; + } + + private ChapterInfo GetChapterInfo(MediaChapter chapter) + { + var info = new ChapterInfo(); + + if (chapter.tags != null) + { + string name; + if (chapter.tags.TryGetValue("title", out name)) + { + info.Name = name; + } + } + + // Limit accuracy to milliseconds to match xml saving + var secondsString = chapter.start_time; + double seconds; + + if (double.TryParse(secondsString, NumberStyles.Any, CultureInfo.InvariantCulture, out seconds)) + { + var ms = Math.Round(TimeSpan.FromSeconds(seconds).TotalMilliseconds); + info.StartPositionTicks = TimeSpan.FromMilliseconds(ms).Ticks; + } + + return info; + } + + private const int MaxSubtitleDescriptionExtractionLength = 100; // When extracting subtitles, the maximum length to consider (to avoid invalid filenames) + + private void FetchWtvInfo(Model.Entities.MediaInfo video, InternalMediaInfoResult data) + { + if (data.format == null || data.format.tags == null) + { + return; + } + + var genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/Genre"); + + if (!string.IsNullOrWhiteSpace(genres)) + { + //genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "genre"); + } + + if (!string.IsNullOrWhiteSpace(genres)) + { + video.Genres = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries) + .Where(i => !string.IsNullOrWhiteSpace(i)) + .Select(i => i.Trim()) + .ToList(); + } + + var officialRating = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/ParentalRating"); + + if (!string.IsNullOrWhiteSpace(officialRating)) + { + video.OfficialRating = officialRating; + } + + var people = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaCredits"); + + if (!string.IsNullOrEmpty(people)) + { + video.People = people.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries) + .Where(i => !string.IsNullOrWhiteSpace(i)) + .Select(i => new BaseItemPerson { Name = i.Trim(), Type = PersonType.Actor }) + .ToList(); + } + + var year = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/OriginalReleaseTime"); + if (!string.IsNullOrWhiteSpace(year)) + { + int val; + + if (int.TryParse(year, NumberStyles.Integer, _usCulture, out val)) + { + video.ProductionYear = val; + } + } + + var premiereDateString = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaOriginalBroadcastDateTime"); + if (!string.IsNullOrWhiteSpace(premiereDateString)) + { + DateTime val; + + // Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/ + // DateTime is reported along with timezone info (typically Z i.e. UTC hence assume None) + if (DateTime.TryParse(year, null, DateTimeStyles.None, out val)) + { + video.PremiereDate = val.ToUniversalTime(); + } + } + + var description = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitleDescription"); + + var subTitle = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitle"); + + // For below code, credit to MCEBuddy: https://mcebuddy2x.codeplex.com/ + + // Sometimes for TV Shows the Subtitle field is empty and the subtitle description contains the subtitle, extract if possible. See ticket https://mcebuddy2x.codeplex.com/workitem/1910 + // The format is -> EPISODE/TOTAL_EPISODES_IN_SEASON. SUBTITLE: DESCRIPTION + // OR -> COMMENT. SUBTITLE: DESCRIPTION + // e.g. -> 4/13. The Doctor's Wife: Science fiction drama. When he follows a Time Lord distress signal, the Doctor puts Amy, Rory and his beloved TARDIS in grave danger. Also in HD. [AD,S] + // e.g. -> CBeebies Bedtime Hour. The Mystery: Animated adventures of two friends who live on an island in the middle of the big city. Some of Abney and Teal's favourite objects are missing. [S] + if (String.IsNullOrWhiteSpace(subTitle) && !String.IsNullOrWhiteSpace(description) && description.Substring(0, Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)).Contains(":")) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename + { + string[] parts = description.Split(':'); + if (parts.Length > 0) + { + string subtitle = parts[0]; + try + { + if (subtitle.Contains("/")) // It contains a episode number and season number + { + string[] numbers = subtitle.Split(' '); + video.IndexNumber = int.Parse(numbers[0].Replace(".", "").Split('/')[0]); + int totalEpisodesInSeason = int.Parse(numbers[0].Replace(".", "").Split('/')[1]); + + description = String.Join(" ", numbers, 1, numbers.Length - 1).Trim(); // Skip the first, concatenate the rest, clean up spaces and save it + } + else + throw new Exception(); // Switch to default parsing + } + catch // Default parsing + { + if (subtitle.Contains(".")) // skip the comment, keep the subtitle + description = String.Join(".", subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first + else + description = subtitle.Trim(); // Clean up whitespaces and save it + } + } + } + + if (!string.IsNullOrWhiteSpace(description)) + { + video.Overview = description; + } + } + + private void ExtractTimestamp(Model.Entities.MediaInfo video) + { + if (video.VideoType == VideoType.VideoFile) + { + if (string.Equals(video.Container, "mpeg2ts", StringComparison.OrdinalIgnoreCase) || + string.Equals(video.Container, "m2ts", StringComparison.OrdinalIgnoreCase) || + string.Equals(video.Container, "ts", StringComparison.OrdinalIgnoreCase)) + { + try + { + video.Timestamp = GetMpegTimestamp(video.Path); + + _logger.Debug("Video has {0} timestamp", video.Timestamp); + } + catch (Exception ex) + { + _logger.ErrorException("Error extracting timestamp info from {0}", ex, video.Path); + video.Timestamp = null; + } + } + } + } + + private TransportStreamTimestamp GetMpegTimestamp(string path) + { + var packetBuffer = new byte['Å']; + + using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + fs.Read(packetBuffer, 0, packetBuffer.Length); + } + + if (packetBuffer[0] == 71) + { + return TransportStreamTimestamp.None; + } + + if ((packetBuffer[4] == 71) && (packetBuffer['Ä'] == 71)) + { + if ((packetBuffer[0] == 0) && (packetBuffer[1] == 0) && (packetBuffer[2] == 0) && (packetBuffer[3] == 0)) + { + return TransportStreamTimestamp.Zero; + } + + return TransportStreamTimestamp.Valid; + } + + return TransportStreamTimestamp.None; + } + + private void UpdateFromMediaInfo(MediaSourceInfo video, MediaStream videoStream) + { + if (video.VideoType == VideoType.VideoFile && video.Protocol == MediaProtocol.File) + { + if (videoStream != null) + { + try + { + var result = new MediaInfoLib().GetVideoInfo(video.Path); + + videoStream.IsCabac = result.IsCabac ?? videoStream.IsCabac; + videoStream.IsInterlaced = result.IsInterlaced ?? videoStream.IsInterlaced; + videoStream.BitDepth = result.BitDepth ?? videoStream.BitDepth; + videoStream.RefFrames = result.RefFrames; + } + catch (Exception ex) + { + _logger.ErrorException("Error running MediaInfo on {0}", ex, video.Path); + } + } + } + } + } +} -- cgit v1.2.3 From 30104bd8de62715d127823e69dc0de9e65d99840 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sun, 5 Apr 2015 11:01:57 -0400 Subject: probe live streams after opening --- MediaBrowser.Api/Sync/SyncService.cs | 11 +- .../Entities/Audio/MusicArtist.cs | 1 + MediaBrowser.Controller/Entities/Folder.cs | 3 +- MediaBrowser.Controller/Entities/Movies/BoxSet.cs | 18 +- MediaBrowser.Controller/Entities/PhotoAlbum.cs | 36 +- .../Entities/UserViewBuilder.cs | 2 +- MediaBrowser.Controller/IO/ThrottledStream.cs | 393 +++++++++++++++++++++ .../MediaBrowser.Controller.csproj | 3 + .../MediaEncoding/IMediaEncoder.cs | 8 +- .../MediaEncoding/MediaInfoRequest.cs | 24 ++ .../Sync/IRemoteSyncProvider.cs | 10 + MediaBrowser.Controller/Sync/ISyncManager.cs | 14 + .../ContentDirectory/ControlHandler.cs | 8 +- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 20 +- .../Probing/ProbeResultNormalizer.cs | 18 +- .../MediaBrowser.Model.Portable.csproj | 6 +- .../MediaBrowser.Model.net35.csproj | 6 +- MediaBrowser.Model/Entities/MediaInfo.cs | 65 ---- MediaBrowser.Model/MediaBrowser.Model.csproj | 2 +- MediaBrowser.Model/MediaInfo/MediaInfo.cs | 66 ++++ MediaBrowser.Model/Sync/SyncOptions.cs | 1 + MediaBrowser.Model/Sync/SyncQualityOption.cs | 5 + MediaBrowser.Model/Users/UserPolicy.cs | 10 +- .../BoxSets/BoxSetMetadataService.cs | 6 +- .../MediaInfo/FFProbeAudioInfo.cs | 17 +- .../MediaInfo/FFProbeVideoInfo.cs | 23 +- .../Collections/ManualCollectionsFolder.cs | 4 +- .../HttpServer/ThrottledStream.cs | 393 --------------------- .../Library/MediaSourceManager.cs | 27 +- .../Library/Resolvers/Movies/MovieResolver.cs | 2 +- .../LiveTv/LiveTvMediaSourceProvider.cs | 10 +- .../Localization/Server/server.json | 8 +- .../MediaBrowser.Server.Implementations.csproj | 1 - .../Photos/PhotoAlbumImageProvider.cs | 2 +- .../Sync/AppSyncProvider.cs | 2 +- .../Sync/MediaSync.cs | 27 +- .../Sync/MultiProviderSync.cs | 9 +- .../Sync/ServerSyncScheduledTask.cs | 9 +- .../Sync/SyncManager.cs | 56 ++- Nuget/MediaBrowser.Common.Internal.nuspec | 4 +- Nuget/MediaBrowser.Common.nuspec | 2 +- Nuget/MediaBrowser.Model.Signed.nuspec | 2 +- Nuget/MediaBrowser.Server.Core.nuspec | 4 +- 43 files changed, 750 insertions(+), 588 deletions(-) create mode 100644 MediaBrowser.Controller/IO/ThrottledStream.cs create mode 100644 MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs create mode 100644 MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs delete mode 100644 MediaBrowser.Model/Entities/MediaInfo.cs create mode 100644 MediaBrowser.Model/MediaInfo/MediaInfo.cs delete mode 100644 MediaBrowser.Server.Implementations/HttpServer/ThrottledStream.cs (limited to 'MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs') diff --git a/MediaBrowser.Api/Sync/SyncService.cs b/MediaBrowser.Api/Sync/SyncService.cs index b9dbf5946..d5f88e6a4 100644 --- a/MediaBrowser.Api/Sync/SyncService.cs +++ b/MediaBrowser.Api/Sync/SyncService.cs @@ -248,6 +248,9 @@ namespace MediaBrowser.Api.Sync result.Targets = _syncManager.GetSyncTargets(request.UserId) .ToList(); + var auth = AuthorizationContext.GetAuthorizationInfo(Request); + var authenticatedUser = _userManager.GetUserById(auth.UserId); + if (!string.IsNullOrWhiteSpace(request.TargetId)) { result.Targets = result.Targets @@ -255,11 +258,11 @@ namespace MediaBrowser.Api.Sync .ToList(); result.QualityOptions = _syncManager - .GetQualityOptions(request.TargetId) + .GetQualityOptions(request.TargetId, authenticatedUser) .ToList(); result.ProfileOptions = _syncManager - .GetProfileOptions(request.TargetId) + .GetProfileOptions(request.TargetId, authenticatedUser) .ToList(); } @@ -277,10 +280,6 @@ namespace MediaBrowser.Api.Sync } }; - var auth = AuthorizationContext.GetAuthorizationInfo(Request); - - var authenticatedUser = _userManager.GetUserById(auth.UserId); - var items = request.ItemIds.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(_libraryManager.GetItemById) .Where(i => i != null); diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 4185590ab..e0c14821e 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -2,6 +2,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; using MediaBrowser.Model.Users; using System; using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 14095f7ff..61e5acdb3 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -991,8 +991,9 @@ namespace MediaBrowser.Controller.Entities } var locations = user.RootFolder - .GetChildren(user, true) + .Children .OfType() + .Where(i => i.IsVisible(user)) .SelectMany(i => i.PhysicalLocations) .ToList(); diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 0778643da..02e9d4cf9 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -175,19 +175,19 @@ namespace MediaBrowser.Controller.Entities.Movies public override bool IsVisible(User user) { - if (base.IsVisible(user)) - { - var userId = user.Id.ToString("N"); - - // Need to check Count > 0 for boxsets created prior to the introduction of Shares - if (Shares.Count > 0 && !Shares.Any(i => string.Equals(userId, i.UserId, StringComparison.OrdinalIgnoreCase))) - { - //return false; - } + var userId = user.Id.ToString("N"); + // Need to check Count > 0 for boxsets created prior to the introduction of Shares + if (Shares.Count > 0 && Shares.Any(i => string.Equals(userId, i.UserId, StringComparison.OrdinalIgnoreCase))) + { return true; } + if (base.IsVisible(user)) + { + return GetChildren(user, true).Any(); + } + return false; } } diff --git a/MediaBrowser.Controller/Entities/PhotoAlbum.cs b/MediaBrowser.Controller/Entities/PhotoAlbum.cs index 24ebf8815..5b48a70e9 100644 --- a/MediaBrowser.Controller/Entities/PhotoAlbum.cs +++ b/MediaBrowser.Controller/Entities/PhotoAlbum.cs @@ -1,11 +1,15 @@ -using MediaBrowser.Model.Configuration; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Users; +using System; using System.Linq; using System.Runtime.Serialization; -using MediaBrowser.Model.Users; +using System.Threading; +using System.Threading.Tasks; namespace MediaBrowser.Controller.Entities { - public class PhotoAlbum : Folder + public class PhotoAlbum : Folder, IMetadataContainer { public override bool SupportsLocalMetadata { @@ -28,5 +32,31 @@ namespace MediaBrowser.Controller.Entities { return config.BlockUnratedItems.Contains(UnratedItem.Other); } + + public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress progress, CancellationToken cancellationToken) + { + var items = GetRecursiveChildren().ToList(); + + var totalItems = items.Count; + var numComplete = 0; + + // Refresh songs + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + + numComplete++; + double percent = numComplete; + percent /= totalItems; + progress.Report(percent * 100); + } + + // Refresh current item + await RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + + progress.Report(100); + } } } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 0e602dabe..c01814bce 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -259,7 +259,7 @@ namespace MediaBrowser.Controller.Entities list.Add(await GetUserView(SpecialFolder.MusicLatest, user, "0", parent).ConfigureAwait(false)); list.Add(await GetUserView(SpecialFolder.MusicAlbums, user, "1", parent).ConfigureAwait(false)); list.Add(await GetUserView(SpecialFolder.MusicAlbumArtists, user, "2", parent).ConfigureAwait(false)); - list.Add(await GetUserView(SpecialFolder.MusicArtists, user, "3", parent).ConfigureAwait(false)); + //list.Add(await GetUserView(SpecialFolder.MusicArtists, user, "3", parent).ConfigureAwait(false)); list.Add(await GetUserView(SpecialFolder.MusicSongs, user, "4", parent).ConfigureAwait(false)); list.Add(await GetUserView(SpecialFolder.MusicGenres, user, "5", parent).ConfigureAwait(false)); list.Add(await GetUserView(SpecialFolder.MusicFavorites, user, "6", parent).ConfigureAwait(false)); diff --git a/MediaBrowser.Controller/IO/ThrottledStream.cs b/MediaBrowser.Controller/IO/ThrottledStream.cs new file mode 100644 index 000000000..1df00b45a --- /dev/null +++ b/MediaBrowser.Controller/IO/ThrottledStream.cs @@ -0,0 +1,393 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.IO +{ + /// + /// Class for streaming data with throttling support. + /// + public class ThrottledStream : Stream + { + /// + /// A constant used to specify an infinite number of bytes that can be transferred per second. + /// + public const long Infinite = 0; + + #region Private members + /// + /// The base stream. + /// + private readonly Stream _baseStream; + + /// + /// The maximum bytes per second that can be transferred through the base stream. + /// + private long _maximumBytesPerSecond; + + /// + /// The number of bytes that has been transferred since the last throttle. + /// + private long _byteCount; + + /// + /// The start time in milliseconds of the last throttle. + /// + private long _start; + #endregion + + #region Properties + /// + /// Gets the current milliseconds. + /// + /// The current milliseconds. + protected long CurrentMilliseconds + { + get + { + return Environment.TickCount; + } + } + + /// + /// Gets or sets the maximum bytes per second that can be transferred through the base stream. + /// + /// The maximum bytes per second. + public long MaximumBytesPerSecond + { + get + { + return _maximumBytesPerSecond; + } + set + { + if (MaximumBytesPerSecond != value) + { + _maximumBytesPerSecond = value; + Reset(); + } + } + } + + /// + /// Gets a value indicating whether the current stream supports reading. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead + { + get + { + return _baseStream.CanRead; + } + } + + /// + /// Gets a value indicating whether the current stream supports seeking. + /// + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek + { + get + { + return _baseStream.CanSeek; + } + } + + /// + /// Gets a value indicating whether the current stream supports writing. + /// + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite + { + get + { + return _baseStream.CanWrite; + } + } + + /// + /// Gets the length in bytes of the stream. + /// + /// + /// A long value representing the length of the stream in bytes. + /// The base stream does not support seeking. + /// Methods were called after the stream was closed. + public override long Length + { + get + { + return _baseStream.Length; + } + } + + /// + /// Gets or sets the position within the current stream. + /// + /// + /// The current position within the stream. + /// An I/O error occurs. + /// The base stream does not support seeking. + /// Methods were called after the stream was closed. + public override long Position + { + get + { + return _baseStream.Position; + } + set + { + _baseStream.Position = value; + } + } + #endregion + + public long MinThrottlePosition; + + #region Ctor + /// + /// Initializes a new instance of the class. + /// + /// The base stream. + /// The maximum bytes per second that can be transferred through the base stream. + /// Thrown when is a null reference. + /// Thrown when is a negative value. + public ThrottledStream(Stream baseStream, long maximumBytesPerSecond) + { + if (baseStream == null) + { + throw new ArgumentNullException("baseStream"); + } + + if (maximumBytesPerSecond < 0) + { + throw new ArgumentOutOfRangeException("maximumBytesPerSecond", + maximumBytesPerSecond, "The maximum number of bytes per second can't be negative."); + } + + _baseStream = baseStream; + _maximumBytesPerSecond = maximumBytesPerSecond; + _start = CurrentMilliseconds; + _byteCount = 0; + } + #endregion + + #region Public methods + /// + /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device. + /// + /// An I/O error occurs. + public override void Flush() + { + _baseStream.Flush(); + } + + /// + /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. + /// + /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in buffer at which to begin storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// + /// The sum of offset and count is larger than the buffer length. + /// Methods were called after the stream was closed. + /// The base stream does not support reading. + /// buffer is null. + /// An I/O error occurs. + /// offset or count is negative. + public override int Read(byte[] buffer, int offset, int count) + { + Throttle(count); + + return _baseStream.Read(buffer, offset, count); + } + + /// + /// Sets the position within the current stream. + /// + /// A byte offset relative to the origin parameter. + /// A value of type indicating the reference point used to obtain the new position. + /// + /// The new position within the current stream. + /// + /// An I/O error occurs. + /// The base stream does not support seeking, such as if the stream is constructed from a pipe or console output. + /// Methods were called after the stream was closed. + public override long Seek(long offset, SeekOrigin origin) + { + return _baseStream.Seek(offset, origin); + } + + /// + /// Sets the length of the current stream. + /// + /// The desired length of the current stream in bytes. + /// The base stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output. + /// An I/O error occurs. + /// Methods were called after the stream was closed. + public override void SetLength(long value) + { + _baseStream.SetLength(value); + } + + private long _bytesWritten; + + /// + /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies count bytes from buffer to the current stream. + /// The zero-based byte offset in buffer at which to begin copying bytes to the current stream. + /// The number of bytes to be written to the current stream. + /// An I/O error occurs. + /// The base stream does not support writing. + /// Methods were called after the stream was closed. + /// buffer is null. + /// The sum of offset and count is greater than the buffer length. + /// offset or count is negative. + public override void Write(byte[] buffer, int offset, int count) + { + Throttle(count); + + _baseStream.Write(buffer, offset, count); + + _bytesWritten += count; + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + await ThrottleAsync(count, cancellationToken).ConfigureAwait(false); + + await _baseStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + + _bytesWritten += count; + } + + /// + /// Returns a that represents the current . + /// + /// + /// A that represents the current . + /// + public override string ToString() + { + return _baseStream.ToString(); + } + #endregion + + private bool ThrottleCheck(int bufferSizeInBytes) + { + if (_bytesWritten < MinThrottlePosition) + { + return false; + } + + // Make sure the buffer isn't empty. + if (_maximumBytesPerSecond <= 0 || bufferSizeInBytes <= 0) + { + return false; + } + + return true; + } + + #region Protected methods + /// + /// Throttles for the specified buffer size in bytes. + /// + /// The buffer size in bytes. + protected void Throttle(int bufferSizeInBytes) + { + if (!ThrottleCheck(bufferSizeInBytes)) + { + return ; + } + + _byteCount += bufferSizeInBytes; + long elapsedMilliseconds = CurrentMilliseconds - _start; + + if (elapsedMilliseconds > 0) + { + // Calculate the current bps. + long bps = _byteCount * 1000L / elapsedMilliseconds; + + // If the bps are more then the maximum bps, try to throttle. + if (bps > _maximumBytesPerSecond) + { + // Calculate the time to sleep. + long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond; + int toSleep = (int)(wakeElapsed - elapsedMilliseconds); + + if (toSleep > 1) + { + try + { + // The time to sleep is more then a millisecond, so sleep. + Thread.Sleep(toSleep); + } + catch (ThreadAbortException) + { + // Eatup ThreadAbortException. + } + + // A sleep has been done, reset. + Reset(); + } + } + } + } + + protected async Task ThrottleAsync(int bufferSizeInBytes, CancellationToken cancellationToken) + { + if (!ThrottleCheck(bufferSizeInBytes)) + { + return; + } + + _byteCount += bufferSizeInBytes; + long elapsedMilliseconds = CurrentMilliseconds - _start; + + if (elapsedMilliseconds > 0) + { + // Calculate the current bps. + long bps = _byteCount * 1000L / elapsedMilliseconds; + + // If the bps are more then the maximum bps, try to throttle. + if (bps > _maximumBytesPerSecond) + { + // Calculate the time to sleep. + long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond; + int toSleep = (int)(wakeElapsed - elapsedMilliseconds); + + if (toSleep > 1) + { + // The time to sleep is more then a millisecond, so sleep. + await Task.Delay(toSleep, cancellationToken).ConfigureAwait(false); + + // A sleep has been done, reset. + Reset(); + } + } + } + } + + /// + /// Will reset the bytecount to 0 and reset the start time to the current time. + /// + protected void Reset() + { + long difference = CurrentMilliseconds - _start; + + // Only reset counters when a known history is available of more then 1 second. + if (difference > 1000) + { + _byteCount = 0; + _start = CurrentMilliseconds; + } + } + #endregion + } +} \ No newline at end of file diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 9a4a2cb62..8c4154966 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -171,6 +171,7 @@ + @@ -212,6 +213,7 @@ + @@ -393,6 +395,7 @@ + diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index bb5674864..5bec7980a 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -67,14 +67,10 @@ namespace MediaBrowser.Controller.MediaEncoding /// /// Gets the media info. /// - /// The input files. - /// The primary path. - /// The protocol. - /// if set to true [is audio]. - /// if set to true [extract chapters]. + /// The request. /// The cancellation token. /// Task. - Task GetMediaInfo(string[] inputFiles, string primaryPath, MediaProtocol protocol, bool isAudio, bool extractChapters, CancellationToken cancellationToken); + Task GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken); /// /// Gets the probe size argument. diff --git a/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs new file mode 100644 index 000000000..ca0c2fdbb --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs @@ -0,0 +1,24 @@ +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.MediaEncoding +{ + public class MediaInfoRequest + { + public string InputPath { get; set; } + public MediaProtocol Protocol { get; set; } + public bool ExtractChapters { get; set; } + public DlnaProfileType MediaType { get; set; } + public IIsoMount MountedIso { get; set; } + public VideoType VideoType { get; set; } + public List PlayableStreamFileNames { get; set; } + + public MediaInfoRequest() + { + PlayableStreamFileNames = new List(); + } + } +} diff --git a/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs b/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs new file mode 100644 index 000000000..aeb7a3bff --- /dev/null +++ b/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs @@ -0,0 +1,10 @@ + +namespace MediaBrowser.Controller.Sync +{ + /// + /// A marker interface + /// + public interface IRemoteSyncProvider + { + } +} diff --git a/MediaBrowser.Controller/Sync/ISyncManager.cs b/MediaBrowser.Controller/Sync/ISyncManager.cs index 3b6e20edc..97591551c 100644 --- a/MediaBrowser.Controller/Sync/ISyncManager.cs +++ b/MediaBrowser.Controller/Sync/ISyncManager.cs @@ -174,6 +174,13 @@ namespace MediaBrowser.Controller.Sync /// The target identifier. /// IEnumerable<SyncQualityOption>. IEnumerable GetQualityOptions(string targetId); + /// + /// Gets the quality options. + /// + /// The target identifier. + /// The user. + /// IEnumerable<SyncQualityOption>. + IEnumerable GetQualityOptions(string targetId, User user); /// /// Gets the profile options. @@ -181,5 +188,12 @@ namespace MediaBrowser.Controller.Sync /// The target identifier. /// IEnumerable<SyncQualityOption>. IEnumerable GetProfileOptions(string targetId); + /// + /// Gets the profile options. + /// + /// The target identifier. + /// The user. + /// IEnumerable<SyncProfileOption>. + IEnumerable GetProfileOptions(string targetId, User user); } } diff --git a/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs b/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs index 5ccea52ba..abd649ad7 100644 --- a/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs +++ b/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs @@ -223,7 +223,7 @@ namespace MediaBrowser.Dlna.ContentDirectory if (string.Equals(flag, "BrowseMetadata")) { totalCount = 1; - + if (item.IsFolder || serverItem.StubType.HasValue) { var childrenResult = (await GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount).ConfigureAwait(false)); @@ -350,7 +350,7 @@ namespace MediaBrowser.Dlna.ContentDirectory }; } - private async Task> GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit) + private Task> GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit) { var folder = (Folder)item; @@ -389,7 +389,7 @@ namespace MediaBrowser.Dlna.ContentDirectory isFolder = true; } - return await folder.GetItems(new InternalItemsQuery + return folder.GetItems(new InternalItemsQuery { Limit = limit, StartIndex = startIndex, @@ -401,7 +401,7 @@ namespace MediaBrowser.Dlna.ContentDirectory IsFolder = isFolder, MediaTypes = mediaTypes.ToArray() - }).ConfigureAwait(false); + }); } private async Task> GetUserItems(BaseItem item, StubType? stubType, User user, SortCriteria sort, int? startIndex, int? limit) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 18d9ccece..be636c0ba 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -6,6 +6,7 @@ using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Session; using MediaBrowser.MediaEncoding.Probing; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; @@ -103,18 +104,17 @@ namespace MediaBrowser.MediaEncoding.Encoder /// /// Gets the media info. /// - /// The input files. - /// The primary path. - /// The protocol. - /// if set to true [is audio]. - /// if set to true [extract chapters]. + /// The request. /// The cancellation token. /// Task. - public Task GetMediaInfo(string[] inputFiles, string primaryPath, MediaProtocol protocol, bool isAudio, - bool extractChapters, CancellationToken cancellationToken) + public Task GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken) { - return GetMediaInfoInternal(GetInputArgument(inputFiles, protocol), primaryPath, protocol, !isAudio && extractChapters, - GetProbeSizeArgument(inputFiles, protocol), isAudio, cancellationToken); + var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters; + + var inputFiles = MediaEncoderHelpers.GetInputArgument(request.InputPath, request.Protocol, request.MountedIso, request.PlayableStreamFileNames); + + return GetMediaInfoInternal(GetInputArgument(inputFiles, request.Protocol), request.InputPath, request.Protocol, extractChapters, + GetProbeSizeArgument(inputFiles, request.Protocol), request.MediaType == DlnaProfileType.Audio, cancellationToken); } /// @@ -152,7 +152,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// The cancellation token. /// Task{MediaInfoResult}. /// - private async Task GetMediaInfoInternal(string inputPath, string primaryPath, MediaProtocol protocol, bool extractChapters, + private async Task GetMediaInfoInternal(string inputPath, string primaryPath, MediaProtocol protocol, bool extractChapters, string probeSizeArgument, bool isAudio, CancellationToken cancellationToken) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 48e8b6ee2..7df3cb56f 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -25,9 +25,9 @@ namespace MediaBrowser.MediaEncoding.Probing _fileSystem = fileSystem; } - public Model.Entities.MediaInfo GetMediaInfo(InternalMediaInfoResult data, bool isAudio, string path, MediaProtocol protocol) + public Model.MediaInfo.MediaInfo GetMediaInfo(InternalMediaInfoResult data, bool isAudio, string path, MediaProtocol protocol) { - var info = new Model.Entities.MediaInfo + var info = new Model.MediaInfo.MediaInfo { Path = path, Protocol = protocol @@ -342,7 +342,7 @@ namespace MediaBrowser.MediaEncoding.Probing return null; } - private void SetAudioRuntimeTicks(InternalMediaInfoResult result, Model.Entities.MediaInfo data) + private void SetAudioRuntimeTicks(InternalMediaInfoResult result, Model.MediaInfo.MediaInfo data) { if (result.streams != null) { @@ -369,7 +369,7 @@ namespace MediaBrowser.MediaEncoding.Probing } } - private void SetSize(InternalMediaInfoResult data, Model.Entities.MediaInfo info) + private void SetSize(InternalMediaInfoResult data, Model.MediaInfo.MediaInfo info) { if (data.format != null) { @@ -384,7 +384,7 @@ namespace MediaBrowser.MediaEncoding.Probing } } - private void SetAudioInfoFromTags(Model.Entities.MediaInfo audio, Dictionary tags) + private void SetAudioInfoFromTags(Model.MediaInfo.MediaInfo audio, Dictionary tags) { var title = FFProbeHelpers.GetDictionaryValue(tags, "title"); @@ -591,7 +591,7 @@ namespace MediaBrowser.MediaEncoding.Probing /// The audio. /// The tags. /// Name of the tag. - private void FetchStudios(Model.Entities.MediaInfo audio, Dictionary tags, string tagName) + private void FetchStudios(Model.MediaInfo.MediaInfo audio, Dictionary tags, string tagName) { var val = FFProbeHelpers.GetDictionaryValue(tags, tagName); @@ -626,7 +626,7 @@ namespace MediaBrowser.MediaEncoding.Probing /// /// The information. /// The tags. - private void FetchGenres(Model.Entities.MediaInfo info, Dictionary tags) + private void FetchGenres(Model.MediaInfo.MediaInfo info, Dictionary tags) { var val = FFProbeHelpers.GetDictionaryValue(tags, "genre"); @@ -697,7 +697,7 @@ namespace MediaBrowser.MediaEncoding.Probing private const int MaxSubtitleDescriptionExtractionLength = 100; // When extracting subtitles, the maximum length to consider (to avoid invalid filenames) - private void FetchWtvInfo(Model.Entities.MediaInfo video, InternalMediaInfoResult data) + private void FetchWtvInfo(Model.MediaInfo.MediaInfo video, InternalMediaInfoResult data) { if (data.format == null || data.format.tags == null) { @@ -806,7 +806,7 @@ namespace MediaBrowser.MediaEncoding.Probing } } - private void ExtractTimestamp(Model.Entities.MediaInfo video) + private void ExtractTimestamp(Model.MediaInfo.MediaInfo video) { if (video.VideoType == VideoType.VideoFile) { diff --git a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj index fdedc51a2..c3ec911de 100644 --- a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj +++ b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj @@ -560,9 +560,6 @@ Entities\MBRegistrationRecord.cs - - Entities\MediaInfo.cs - Entities\MediaStream.cs @@ -809,6 +806,9 @@ MediaInfo\LiveStreamResponse.cs + + MediaInfo\MediaInfo.cs + MediaInfo\MediaProtocol.cs diff --git a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj index 3618aa972..437fcc8ff 100644 --- a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj +++ b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj @@ -525,9 +525,6 @@ Entities\MBRegistrationRecord.cs - - Entities\MediaInfo.cs - Entities\MediaStream.cs @@ -765,6 +762,9 @@ MediaInfo\LiveStreamResponse.cs + + MediaInfo\MediaInfo.cs + MediaInfo\MediaProtocol.cs diff --git a/MediaBrowser.Model/Entities/MediaInfo.cs b/MediaBrowser.Model/Entities/MediaInfo.cs deleted file mode 100644 index 67efe3108..000000000 --- a/MediaBrowser.Model/Entities/MediaInfo.cs +++ /dev/null @@ -1,65 +0,0 @@ -using MediaBrowser.Model.Dto; -using System; -using System.Collections.Generic; - -namespace MediaBrowser.Model.Entities -{ - public class MediaInfo : MediaSourceInfo, IHasProviderIds - { - public List Chapters { get; set; } - - /// - /// Gets or sets the title. - /// - /// The title. - public string Title { get; set; } - /// - /// Gets or sets the album. - /// - /// The album. - public string Album { get; set; } - /// - /// Gets or sets the artists. - /// - /// The artists. - public List Artists { get; set; } - /// - /// Gets or sets the album artists. - /// - /// The album artists. - public List AlbumArtists { get; set; } - /// - /// Gets or sets the studios. - /// - /// The studios. - public List Studios { get; set; } - public List Genres { get; set; } - public int? IndexNumber { get; set; } - public int? ParentIndexNumber { get; set; } - public int? ProductionYear { get; set; } - public DateTime? PremiereDate { get; set; } - public List People { get; set; } - public Dictionary ProviderIds { get; set; } - /// - /// Gets or sets the official rating. - /// - /// The official rating. - public string OfficialRating { get; set; } - /// - /// Gets or sets the overview. - /// - /// The overview. - public string Overview { get; set; } - - public MediaInfo() - { - Chapters = new List(); - Artists = new List(); - AlbumArtists = new List(); - Studios = new List(); - Genres = new List(); - People = new List(); - ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - } -} \ No newline at end of file diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 067309512..2a210b7a2 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -226,7 +226,7 @@ - + diff --git a/MediaBrowser.Model/MediaInfo/MediaInfo.cs b/MediaBrowser.Model/MediaInfo/MediaInfo.cs new file mode 100644 index 000000000..21f258693 --- /dev/null +++ b/MediaBrowser.Model/MediaInfo/MediaInfo.cs @@ -0,0 +1,66 @@ +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Model.MediaInfo +{ + public class MediaInfo : MediaSourceInfo, IHasProviderIds + { + public List Chapters { get; set; } + + /// + /// Gets or sets the title. + /// + /// The title. + public string Title { get; set; } + /// + /// Gets or sets the album. + /// + /// The album. + public string Album { get; set; } + /// + /// Gets or sets the artists. + /// + /// The artists. + public List Artists { get; set; } + /// + /// Gets or sets the album artists. + /// + /// The album artists. + public List AlbumArtists { get; set; } + /// + /// Gets or sets the studios. + /// + /// The studios. + public List Studios { get; set; } + public List Genres { get; set; } + public int? IndexNumber { get; set; } + public int? ParentIndexNumber { get; set; } + public int? ProductionYear { get; set; } + public DateTime? PremiereDate { get; set; } + public List People { get; set; } + public Dictionary ProviderIds { get; set; } + /// + /// Gets or sets the official rating. + /// + /// The official rating. + public string OfficialRating { get; set; } + /// + /// Gets or sets the overview. + /// + /// The overview. + public string Overview { get; set; } + + public MediaInfo() + { + Chapters = new List(); + Artists = new List(); + AlbumArtists = new List(); + Studios = new List(); + Genres = new List(); + People = new List(); + ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Model/Sync/SyncOptions.cs b/MediaBrowser.Model/Sync/SyncOptions.cs index 294f7bcef..765dea86b 100644 --- a/MediaBrowser.Model/Sync/SyncOptions.cs +++ b/MediaBrowser.Model/Sync/SyncOptions.cs @@ -4,5 +4,6 @@ namespace MediaBrowser.Model.Sync public class SyncOptions { public string TemporaryPath { get; set; } + public long UploadSpeedLimitBytes { get; set; } } } diff --git a/MediaBrowser.Model/Sync/SyncQualityOption.cs b/MediaBrowser.Model/Sync/SyncQualityOption.cs index 597b98727..6eff4b9a4 100644 --- a/MediaBrowser.Model/Sync/SyncQualityOption.cs +++ b/MediaBrowser.Model/Sync/SyncQualityOption.cs @@ -23,5 +23,10 @@ namespace MediaBrowser.Model.Sync /// /// true if this instance is default; otherwise, false. public bool IsDefault { get; set; } + /// + /// Gets or sets a value indicating whether this instance is original quality. + /// + /// true if this instance is original quality; otherwise, false. + public bool IsOriginalQuality { get; set; } } } diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 640f03e2a..b3c599496 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -39,6 +39,8 @@ namespace MediaBrowser.Model.Users public bool EnableLiveTvAccess { get; set; } public bool EnableMediaPlayback { get; set; } + public bool EnableMediaPlaybackTranscoding { get; set; } + public bool EnableContentDeletion { get; set; } public bool EnableContentDownloading { get; set; } @@ -47,6 +49,7 @@ namespace MediaBrowser.Model.Users /// /// true if [enable synchronize]; otherwise, false. public bool EnableSync { get; set; } + public bool EnableSyncTranscoding { get; set; } public string[] EnabledDevices { get; set; } public bool EnableAllDevices { get; set; } @@ -62,9 +65,14 @@ namespace MediaBrowser.Model.Users public UserPolicy() { EnableSync = true; - EnableLiveTvManagement = true; + EnableSyncTranscoding = true; + EnableMediaPlayback = true; + EnableMediaPlaybackTranscoding = true; + + EnableLiveTvManagement = true; EnableLiveTvAccess = true; + EnableSharedDeviceControl = true; BlockedTags = new string[] { }; diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs index 3ac3cccb3..92327c9bc 100644 --- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs +++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs @@ -3,7 +3,6 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; @@ -16,11 +15,8 @@ namespace MediaBrowser.Providers.BoxSets { public class BoxSetMetadataService : MetadataService { - private readonly ILocalizationManager _iLocalizationManager; - - public BoxSetMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, IFileSystem fileSystem, IUserDataManager userDataManager, ILocalizationManager iLocalizationManager) : base(serverConfigurationManager, logger, providerManager, providerRepo, fileSystem, userDataManager) + public BoxSetMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, IFileSystem fileSystem, IUserDataManager userDataManager) : base(serverConfigurationManager, logger, providerManager, providerRepo, fileSystem, userDataManager) { - _iLocalizationManager = iLocalizationManager; } /// diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs index b1bed7310..3e816802e 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs @@ -4,6 +4,7 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; @@ -53,7 +54,7 @@ namespace MediaBrowser.Providers.MediaInfo private const string SchemaVersion = "2"; - private async Task GetMediaInfo(BaseItem item, CancellationToken cancellationToken) + private async Task GetMediaInfo(BaseItem item, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -64,7 +65,7 @@ namespace MediaBrowser.Providers.MediaInfo try { - return _json.DeserializeFromFile(cachePath); + return _json.DeserializeFromFile(cachePath); } catch (FileNotFoundException) { @@ -74,9 +75,13 @@ namespace MediaBrowser.Providers.MediaInfo { } - var inputPath = new[] { item.Path }; + var result = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest + { + InputPath = item.Path, + MediaType = DlnaProfileType.Audio, + Protocol = MediaProtocol.File - var result = await _mediaEncoder.GetMediaInfo(inputPath, item.Path, MediaProtocol.File, true, false, cancellationToken).ConfigureAwait(false); + }, cancellationToken).ConfigureAwait(false); Directory.CreateDirectory(Path.GetDirectoryName(cachePath)); _json.SerializeToFile(result, cachePath); @@ -91,7 +96,7 @@ namespace MediaBrowser.Providers.MediaInfo /// The cancellation token. /// The media information. /// Task. - protected Task Fetch(Audio audio, CancellationToken cancellationToken, Model.Entities.MediaInfo mediaInfo) + protected Task Fetch(Audio audio, CancellationToken cancellationToken, Model.MediaInfo.MediaInfo mediaInfo) { var mediaStreams = mediaInfo.MediaStreams; @@ -115,7 +120,7 @@ namespace MediaBrowser.Providers.MediaInfo /// /// The audio. /// The data. - private void FetchDataFromTags(Audio audio, Model.Entities.MediaInfo data) + private void FetchDataFromTags(Audio audio, Model.MediaInfo.MediaInfo data) { // Only set Name if title was found in the dictionary if (!string.IsNullOrEmpty(data.Title)) diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index cec66f3c1..c433018c0 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -1,5 +1,6 @@ using DvdLib.Ifo; using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Controller.Chapters; @@ -129,9 +130,9 @@ namespace MediaBrowser.Providers.MediaInfo return ItemUpdateType.MetadataImport; } - private const string SchemaVersion = "1"; + private const string SchemaVersion = "2"; - private async Task GetMediaInfo(Video item, + private async Task GetMediaInfo(Video item, IIsoMount isoMount, CancellationToken cancellationToken) { @@ -144,7 +145,7 @@ namespace MediaBrowser.Providers.MediaInfo try { - return _json.DeserializeFromFile(cachePath); + return _json.DeserializeFromFile(cachePath); } catch (FileNotFoundException) { @@ -158,9 +159,17 @@ namespace MediaBrowser.Providers.MediaInfo ? MediaProtocol.Http : MediaProtocol.File; - var inputPath = MediaEncoderHelpers.GetInputArgument(item.Path, protocol, isoMount, item.PlayableStreamFileNames); + var result = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest + { + PlayableStreamFileNames = item.PlayableStreamFileNames, + MountedIso = isoMount, + ExtractChapters = true, + VideoType = item.VideoType, + MediaType = DlnaProfileType.Video, + InputPath = item.Path, + Protocol = protocol - var result = await _mediaEncoder.GetMediaInfo(inputPath, item.Path, protocol, false, true, cancellationToken).ConfigureAwait(false); + }, cancellationToken).ConfigureAwait(false); Directory.CreateDirectory(Path.GetDirectoryName(cachePath)); _json.SerializeToFile(result, cachePath); @@ -170,7 +179,7 @@ namespace MediaBrowser.Providers.MediaInfo protected async Task Fetch(Video video, CancellationToken cancellationToken, - Model.Entities.MediaInfo mediaInfo, + Model.MediaInfo.MediaInfo mediaInfo, IIsoMount isoMount, BlurayDiscInfo blurayInfo, MetadataRefreshOptions options) @@ -348,7 +357,7 @@ namespace MediaBrowser.Providers.MediaInfo return _blurayExaminer.GetDiscInfo(path); } - private void FetchEmbeddedInfo(Video video, Model.Entities.MediaInfo data) + private void FetchEmbeddedInfo(Video video, Model.MediaInfo.MediaInfo data) { if (!video.LockedFields.Contains(MetadataFields.OfficialRating)) { diff --git a/MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs b/MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs index bbe37cb50..8f5d8fe9b 100644 --- a/MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs +++ b/MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs @@ -14,9 +14,7 @@ namespace MediaBrowser.Server.Implementations.Collections public override bool IsVisible(User user) { - return base.IsVisible(user) && GetChildren(user, false) - .OfType() - .Any(i => i.IsVisible(user)); + return base.IsVisible(user) && GetChildren(user, false).Any(); } public override bool IsHidden diff --git a/MediaBrowser.Server.Implementations/HttpServer/ThrottledStream.cs b/MediaBrowser.Server.Implementations/HttpServer/ThrottledStream.cs deleted file mode 100644 index 1c01fa9e0..000000000 --- a/MediaBrowser.Server.Implementations/HttpServer/ThrottledStream.cs +++ /dev/null @@ -1,393 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.HttpServer -{ - /// - /// Class for streaming data with throttling support. - /// - public class ThrottledStream : Stream - { - /// - /// A constant used to specify an infinite number of bytes that can be transferred per second. - /// - public const long Infinite = 0; - - #region Private members - /// - /// The base stream. - /// - private readonly Stream _baseStream; - - /// - /// The maximum bytes per second that can be transferred through the base stream. - /// - private long _maximumBytesPerSecond; - - /// - /// The number of bytes that has been transferred since the last throttle. - /// - private long _byteCount; - - /// - /// The start time in milliseconds of the last throttle. - /// - private long _start; - #endregion - - #region Properties - /// - /// Gets the current milliseconds. - /// - /// The current milliseconds. - protected long CurrentMilliseconds - { - get - { - return Environment.TickCount; - } - } - - /// - /// Gets or sets the maximum bytes per second that can be transferred through the base stream. - /// - /// The maximum bytes per second. - public long MaximumBytesPerSecond - { - get - { - return _maximumBytesPerSecond; - } - set - { - if (MaximumBytesPerSecond != value) - { - _maximumBytesPerSecond = value; - Reset(); - } - } - } - - /// - /// Gets a value indicating whether the current stream supports reading. - /// - /// true if the stream supports reading; otherwise, false. - public override bool CanRead - { - get - { - return _baseStream.CanRead; - } - } - - /// - /// Gets a value indicating whether the current stream supports seeking. - /// - /// - /// true if the stream supports seeking; otherwise, false. - public override bool CanSeek - { - get - { - return _baseStream.CanSeek; - } - } - - /// - /// Gets a value indicating whether the current stream supports writing. - /// - /// - /// true if the stream supports writing; otherwise, false. - public override bool CanWrite - { - get - { - return _baseStream.CanWrite; - } - } - - /// - /// Gets the length in bytes of the stream. - /// - /// - /// A long value representing the length of the stream in bytes. - /// The base stream does not support seeking. - /// Methods were called after the stream was closed. - public override long Length - { - get - { - return _baseStream.Length; - } - } - - /// - /// Gets or sets the position within the current stream. - /// - /// - /// The current position within the stream. - /// An I/O error occurs. - /// The base stream does not support seeking. - /// Methods were called after the stream was closed. - public override long Position - { - get - { - return _baseStream.Position; - } - set - { - _baseStream.Position = value; - } - } - #endregion - - public long MinThrottlePosition; - - #region Ctor - /// - /// Initializes a new instance of the class. - /// - /// The base stream. - /// The maximum bytes per second that can be transferred through the base stream. - /// Thrown when is a null reference. - /// Thrown when is a negative value. - public ThrottledStream(Stream baseStream, long maximumBytesPerSecond) - { - if (baseStream == null) - { - throw new ArgumentNullException("baseStream"); - } - - if (maximumBytesPerSecond < 0) - { - throw new ArgumentOutOfRangeException("maximumBytesPerSecond", - maximumBytesPerSecond, "The maximum number of bytes per second can't be negative."); - } - - _baseStream = baseStream; - _maximumBytesPerSecond = maximumBytesPerSecond; - _start = CurrentMilliseconds; - _byteCount = 0; - } - #endregion - - #region Public methods - /// - /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device. - /// - /// An I/O error occurs. - public override void Flush() - { - _baseStream.Flush(); - } - - /// - /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. - /// - /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source. - /// The zero-based byte offset in buffer at which to begin storing the data read from the current stream. - /// The maximum number of bytes to be read from the current stream. - /// - /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. - /// - /// The sum of offset and count is larger than the buffer length. - /// Methods were called after the stream was closed. - /// The base stream does not support reading. - /// buffer is null. - /// An I/O error occurs. - /// offset or count is negative. - public override int Read(byte[] buffer, int offset, int count) - { - Throttle(count); - - return _baseStream.Read(buffer, offset, count); - } - - /// - /// Sets the position within the current stream. - /// - /// A byte offset relative to the origin parameter. - /// A value of type indicating the reference point used to obtain the new position. - /// - /// The new position within the current stream. - /// - /// An I/O error occurs. - /// The base stream does not support seeking, such as if the stream is constructed from a pipe or console output. - /// Methods were called after the stream was closed. - public override long Seek(long offset, SeekOrigin origin) - { - return _baseStream.Seek(offset, origin); - } - - /// - /// Sets the length of the current stream. - /// - /// The desired length of the current stream in bytes. - /// The base stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output. - /// An I/O error occurs. - /// Methods were called after the stream was closed. - public override void SetLength(long value) - { - _baseStream.SetLength(value); - } - - private long _bytesWritten; - - /// - /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. - /// - /// An array of bytes. This method copies count bytes from buffer to the current stream. - /// The zero-based byte offset in buffer at which to begin copying bytes to the current stream. - /// The number of bytes to be written to the current stream. - /// An I/O error occurs. - /// The base stream does not support writing. - /// Methods were called after the stream was closed. - /// buffer is null. - /// The sum of offset and count is greater than the buffer length. - /// offset or count is negative. - public override void Write(byte[] buffer, int offset, int count) - { - Throttle(count); - - _baseStream.Write(buffer, offset, count); - - _bytesWritten += count; - } - - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - await ThrottleAsync(count, cancellationToken).ConfigureAwait(false); - - await _baseStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); - - _bytesWritten += count; - } - - /// - /// Returns a that represents the current . - /// - /// - /// A that represents the current . - /// - public override string ToString() - { - return _baseStream.ToString(); - } - #endregion - - private bool ThrottleCheck(int bufferSizeInBytes) - { - if (_bytesWritten < MinThrottlePosition) - { - return false; - } - - // Make sure the buffer isn't empty. - if (_maximumBytesPerSecond <= 0 || bufferSizeInBytes <= 0) - { - return false; - } - - return true; - } - - #region Protected methods - /// - /// Throttles for the specified buffer size in bytes. - /// - /// The buffer size in bytes. - protected void Throttle(int bufferSizeInBytes) - { - if (!ThrottleCheck(bufferSizeInBytes)) - { - return ; - } - - _byteCount += bufferSizeInBytes; - long elapsedMilliseconds = CurrentMilliseconds - _start; - - if (elapsedMilliseconds > 0) - { - // Calculate the current bps. - long bps = _byteCount * 1000L / elapsedMilliseconds; - - // If the bps are more then the maximum bps, try to throttle. - if (bps > _maximumBytesPerSecond) - { - // Calculate the time to sleep. - long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond; - int toSleep = (int)(wakeElapsed - elapsedMilliseconds); - - if (toSleep > 1) - { - try - { - // The time to sleep is more then a millisecond, so sleep. - Thread.Sleep(toSleep); - } - catch (ThreadAbortException) - { - // Eatup ThreadAbortException. - } - - // A sleep has been done, reset. - Reset(); - } - } - } - } - - protected async Task ThrottleAsync(int bufferSizeInBytes, CancellationToken cancellationToken) - { - if (!ThrottleCheck(bufferSizeInBytes)) - { - return; - } - - _byteCount += bufferSizeInBytes; - long elapsedMilliseconds = CurrentMilliseconds - _start; - - if (elapsedMilliseconds > 0) - { - // Calculate the current bps. - long bps = _byteCount * 1000L / elapsedMilliseconds; - - // If the bps are more then the maximum bps, try to throttle. - if (bps > _maximumBytesPerSecond) - { - // Calculate the time to sleep. - long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond; - int toSleep = (int)(wakeElapsed - elapsedMilliseconds); - - if (toSleep > 1) - { - // The time to sleep is more then a millisecond, so sleep. - await Task.Delay(toSleep, cancellationToken).ConfigureAwait(false); - - // A sleep has been done, reset. - Reset(); - } - } - } - } - - /// - /// Will reset the bytecount to 0 and reset the start time to the current time. - /// - protected void Reset() - { - long difference = CurrentMilliseconds - _start; - - // Only reset counters when a known history is available of more then 1 second. - if (difference > 1000) - { - _byteCount = 0; - _start = CurrentMilliseconds; - } - } - #endregion - } -} \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs b/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs index 27a7d4ea9..01efe0ab1 100644 --- a/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs +++ b/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs @@ -137,21 +137,16 @@ namespace MediaBrowser.Server.Implementations.Library public async Task> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, CancellationToken cancellationToken) { var item = _libraryManager.GetItemById(id); - IEnumerable mediaSources; var hasMediaSources = (IHasMediaSources)item; User user = null; - if (string.IsNullOrWhiteSpace(userId)) - { - mediaSources = hasMediaSources.GetMediaSources(enablePathSubstitution); - } - else + if (!string.IsNullOrWhiteSpace(userId)) { user = _userManager.GetUserById(userId); - mediaSources = GetStaticMediaSources(hasMediaSources, enablePathSubstitution, user); } + var mediaSources = GetStaticMediaSources(hasMediaSources, enablePathSubstitution, user); var dynamicMediaSources = await GetDynamicMediaSources(hasMediaSources, cancellationToken).ConfigureAwait(false); var list = new List(); @@ -166,9 +161,11 @@ namespace MediaBrowser.Server.Implementations.Library } if (source.Protocol == MediaProtocol.File) { - source.SupportsDirectStream = File.Exists(source.Path); - // TODO: Path substitution + if (!File.Exists(source.Path)) + { + source.SupportsDirectStream = false; + } } else if (source.Protocol == MediaProtocol.Http) { @@ -183,6 +180,17 @@ namespace MediaBrowser.Server.Implementations.Library list.Add(source); } + foreach (var source in list) + { + if (user != null) + { + if (!user.Policy.EnableMediaPlaybackTranscoding) + { + source.SupportsTranscoding = false; + } + } + } + return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder); } @@ -343,6 +351,7 @@ namespace MediaBrowser.Server.Implementations.Library } var json = _jsonSerializer.SerializeToString(mediaSource); + _logger.Debug("Live stream opened: " + json); var clone = _jsonSerializer.DeserializeFromString(json); if (!string.IsNullOrWhiteSpace(request.UserId)) diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 71daf2b0c..f88293b2a 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -64,7 +64,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)) { - return ResolveVideos