diff --git a/Emby.Drawing/ImageMagick/ImageMagickEncoder.cs b/Emby.Drawing/ImageMagick/ImageMagickEncoder.cs index ff4a8f55bc..78633472b6 100644 --- a/Emby.Drawing/ImageMagick/ImageMagickEncoder.cs +++ b/Emby.Drawing/ImageMagick/ImageMagickEncoder.cs @@ -115,9 +115,17 @@ namespace Emby.Drawing.ImageMagick } } + private bool HasTransparency(string path) + { + var ext = Path.GetExtension(path); + + return string.Equals(ext, ".png", StringComparison.OrdinalIgnoreCase) || + string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase); + } + public void EncodeImage(string inputPath, string outputPath, int width, int height, int quality, ImageProcessingOptions options) { - if (string.IsNullOrWhiteSpace(options.BackgroundColor)) + if (string.IsNullOrWhiteSpace(options.BackgroundColor) || !HasTransparency(inputPath)) { using (var originalImage = new MagickWand(inputPath)) { diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 2cd9007544..05ff503e47 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -250,19 +250,19 @@ namespace MediaBrowser.Api return GetTranscodingJob(path, type) != null; } - public TranscodingJob GetTranscodingJob(string path, TranscodingJobType type) + public TranscodingJob GetTranscodingJobByPlaySessionId(string playSessionId) { lock (_activeTranscodingJobs) { - return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase)); + return _activeTranscodingJobs.FirstOrDefault(j => j.PlaySessionId.Equals(playSessionId, StringComparison.OrdinalIgnoreCase)); } } - public TranscodingJob GetTranscodingJob(string id) + public TranscodingJob GetTranscodingJob(string path, TranscodingJobType type) { lock (_activeTranscodingJobs) { - return _activeTranscodingJobs.FirstOrDefault(j => j.Id.Equals(id, StringComparison.OrdinalIgnoreCase)); + return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase)); } } @@ -339,14 +339,17 @@ namespace MediaBrowser.Api return; } - var timerDuration = job.Type == TranscodingJobType.Progressive ? - 1000 : - 1800000; + var timerDuration = 1000; - // We can really reduce the timeout for apps that are using the newer api - if (!string.IsNullOrWhiteSpace(job.PlaySessionId) && job.Type != TranscodingJobType.Progressive) + if (job.Type != TranscodingJobType.Progressive) { - timerDuration = 50000; + timerDuration = 1800000; + + // We can really reduce the timeout for apps that are using the newer api + if (!string.IsNullOrWhiteSpace(job.PlaySessionId)) + { + timerDuration = 60000; + } } job.PingTimeout = timerDuration; @@ -628,6 +631,9 @@ namespace MediaBrowser.Api /// /// The live stream identifier. public string LiveStreamId { get; set; } + + public bool IsLiveOutput { get; set; } + /// /// Gets or sets the path. /// diff --git a/MediaBrowser.Api/Dlna/DlnaServerService.cs b/MediaBrowser.Api/Dlna/DlnaServerService.cs index bdf7d6b074..4f5e2ab259 100644 --- a/MediaBrowser.Api/Dlna/DlnaServerService.cs +++ b/MediaBrowser.Api/Dlna/DlnaServerService.cs @@ -109,7 +109,7 @@ namespace MediaBrowser.Api.Dlna private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar; // TODO: Add utf-8 - private const string XMLContentType = "text/xml"; + private const string XMLContentType = "text/xml; charset=UTF-8"; public DlnaServerService(IDlnaManager dlnaManager, IContentDirectory contentDirectory, IConnectionManager connectionManager, IMediaReceiverRegistrar mediaReceiverRegistrar) { diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index 639c1f54b0..8c6cc0a18e 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -625,6 +625,8 @@ namespace MediaBrowser.Api.Images var file = await _imageProcessor.ProcessImage(options).ConfigureAwait(false); + headers["Vary"] = "Accept"; + return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions { CacheDuration = cacheDuration, @@ -659,8 +661,10 @@ namespace MediaBrowser.Api.Images return ImageFormat.Png; } - if (string.Equals(Path.GetExtension(image.Path), ".jpg", StringComparison.OrdinalIgnoreCase) || - string.Equals(Path.GetExtension(image.Path), ".jpeg", StringComparison.OrdinalIgnoreCase)) + var extension = Path.GetExtension(image.Path); + + if (string.Equals(extension, ".jpg", StringComparison.OrdinalIgnoreCase) || + string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase)) { return ImageFormat.Jpg; } diff --git a/MediaBrowser.Api/ItemUpdateService.cs b/MediaBrowser.Api/ItemUpdateService.cs index 0138380915..bab02de356 100644 --- a/MediaBrowser.Api/ItemUpdateService.cs +++ b/MediaBrowser.Api/ItemUpdateService.cs @@ -389,22 +389,28 @@ namespace MediaBrowser.Api game.PlayersSupported = request.Players; } - var hasAlbumArtists = item as IHasAlbumArtist; - if (hasAlbumArtists != null) + if (request.AlbumArtists != null) { - hasAlbumArtists.AlbumArtists = request - .AlbumArtists - .Select(i => i.Name) - .ToList(); + var hasAlbumArtists = item as IHasAlbumArtist; + if (hasAlbumArtists != null) + { + hasAlbumArtists.AlbumArtists = request + .AlbumArtists + .Select(i => i.Name) + .ToList(); + } } - var hasArtists = item as IHasArtist; - if (hasArtists != null) + if (request.ArtistItems != null) { - hasArtists.Artists = request - .ArtistItems - .Select(i => i.Name) - .ToList(); + var hasArtists = item as IHasArtist; + if (hasArtists != null) + { + hasArtists.Artists = request + .ArtistItems + .Select(i => i.Name) + .ToList(); + } } var song = item as Audio; diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index ab205d6ebf..0dfd812c3a 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -84,10 +84,28 @@ - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/MediaBrowser.Api/Music/InstantMixService.cs b/MediaBrowser.Api/Music/InstantMixService.cs index 78c6a8bf41..46034dc61a 100644 --- a/MediaBrowser.Api/Music/InstantMixService.cs +++ b/MediaBrowser.Api/Music/InstantMixService.cs @@ -50,7 +50,7 @@ namespace MediaBrowser.Api.Music [Route("/MusicGenres/InstantMix", "GET", Summary = "Creates an instant playlist based on a music genre")] public class GetInstantMixFromMusicGenreId : BaseGetSimilarItems { - [ApiMember(Name = "Id", Description = "The genre Id", IsRequired = true, DataType = "string", ParameterType = "querypath", Verb = "GET")] + [ApiMember(Name = "Id", Description = "The genre Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] public string Id { get; set; } } diff --git a/MediaBrowser.Api/PackageService.cs b/MediaBrowser.Api/PackageService.cs index 1d792fbc16..5ef8b09871 100644 --- a/MediaBrowser.Api/PackageService.cs +++ b/MediaBrowser.Api/PackageService.cs @@ -56,6 +56,8 @@ namespace MediaBrowser.Api [ApiMember(Name = "IsAdult", Description = "Optional. Filter by package that contain adult content.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] public bool? IsAdult { get; set; } + + public bool? IsAppStoreEnabled { get; set; } } /// @@ -207,6 +209,11 @@ namespace MediaBrowser.Api packages = packages.Where(p => p.adult == request.IsAdult.Value); } + if (request.IsAppStoreEnabled.HasValue) + { + packages = packages.Where(p => p.enableInAppStore == request.IsAppStoreEnabled.Value); + } + return ToOptimizedResult(packages.ToList()); } diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 5e06ab1d00..31679aad3c 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -157,11 +157,11 @@ namespace MediaBrowser.Api.Playback /// The fast seek command line parameter. protected string GetFastSeekCommandLineParameter(StreamRequest request) { - var time = request.StartTimeTicks; + var time = request.StartTimeTicks ?? 0; - if (time.HasValue && time.Value > 0) + if (time > 0) { - return string.Format("-ss {0}", MediaEncoder.GetTimeParameter(time.Value)); + return string.Format("-ss {0}", MediaEncoder.GetTimeParameter(time)); } return string.Empty; @@ -690,7 +690,7 @@ namespace MediaBrowser.Api.Playback // TODO: Perhaps also use original_size=1920x800 ?? return string.Format("subtitles=filename='{0}'{1},setpts=PTS -{2}/TB", - subtitlePath.Replace('\\', '/').Replace(":/", "\\:/"), + subtitlePath.Replace("'", "\\'").Replace('\\', '/').Replace(":/", "\\:/"), charsetParam, seconds.ToString(UsCulture)); } @@ -698,7 +698,7 @@ namespace MediaBrowser.Api.Playback var mediaPath = state.MediaPath ?? string.Empty; return string.Format("subtitles='{0}:si={1}',setpts=PTS -{2}/TB", - mediaPath.Replace('\\', '/').Replace(":/", "\\:/"), + mediaPath.Replace("'", "\\'").Replace('\\', '/').Replace(":/", "\\:/"), state.InternalSubtitleStreamOffset.ToString(UsCulture), seconds.ToString(UsCulture)); } @@ -769,26 +769,31 @@ namespace MediaBrowser.Api.Playback /// System.Nullable{System.Int32}. private int? GetNumAudioChannelsParam(StreamRequest request, MediaStream audioStream, string outputAudioCodec) { - if (audioStream != null) - { - var codec = outputAudioCodec ?? string.Empty; + var inputChannels = audioStream == null + ? null + : audioStream.Channels; - if (audioStream.Channels > 2 && codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1) - { - // wmav2 currently only supports two channel output - return 2; - } + var codec = outputAudioCodec ?? string.Empty; + + if (codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1) + { + // wmav2 currently only supports two channel output + return Math.Min(2, inputChannels ?? 2); } if (request.MaxAudioChannels.HasValue) { - if (audioStream != null && audioStream.Channels.HasValue) + if (inputChannels.HasValue) { - return Math.Min(request.MaxAudioChannels.Value, audioStream.Channels.Value); + return Math.Min(request.MaxAudioChannels.Value, inputChannels.Value); } + var channelLimit = codec.IndexOf("mp3", StringComparison.OrdinalIgnoreCase) != -1 + ? 2 + : 5; + // If we don't have any media info then limit it to 5 to prevent encoding errors due to asking for too many channels - return Math.Min(request.MaxAudioChannels.Value, 5); + return Math.Min(request.MaxAudioChannels.Value, channelLimit); } return request.AudioChannels; @@ -1055,7 +1060,7 @@ namespace MediaBrowser.Api.Playback private void StartThrottler(StreamState state, TranscodingJob transcodingJob) { - if (state.InputProtocol == MediaProtocol.File && + if (EnableThrottling(state) && state.InputProtocol == MediaProtocol.File && state.RunTimeTicks.HasValue && state.VideoType == VideoType.VideoFile && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) @@ -1068,6 +1073,11 @@ namespace MediaBrowser.Api.Playback } } + protected virtual bool EnableThrottling(StreamState state) + { + return true; + } + private async void StartStreamingLog(TranscodingJob transcodingJob, StreamState state, Stream source, Stream target) { try @@ -1690,6 +1700,11 @@ namespace MediaBrowser.Api.Playback private void TryStreamCopy(StreamState state, VideoStreamRequest videoRequest) { + if (!EnableStreamCopy) + { + return; + } + if (state.VideoStream != null && CanStreamCopyVideo(videoRequest, state.VideoStream)) { state.OutputVideoCodec = "copy"; @@ -1701,6 +1716,14 @@ namespace MediaBrowser.Api.Playback } } + protected virtual bool EnableStreamCopy + { + get + { + return true; + } + } + private void AttachMediaSourceInfo(StreamState state, MediaSourceInfo mediaSource, VideoStreamRequest videoRequest, diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index b10c02e17c..b2ffeca3db 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -7,13 +7,13 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; +using MediaBrowser.Model.Serialization; using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Api.Playback.Hls { @@ -100,6 +100,7 @@ namespace MediaBrowser.Api.Playback.Hls try { job = await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false); + job.IsLiveOutput = isLive; } catch { @@ -133,7 +134,7 @@ namespace MediaBrowser.Api.Playback.Hls var appendBaselineStream = false; var baselineStreamBitrate = 64000; - var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream; + var hlsVideoRequest = state.VideoRequest as GetHlsVideoStreamLegacy; if (hlsVideoRequest != null) { appendBaselineStream = hlsVideoRequest.AppendBaselineStream; @@ -244,7 +245,7 @@ namespace MediaBrowser.Api.Playback.Hls protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding) { - var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream; + var hlsVideoRequest = state.VideoRequest as GetHlsVideoStreamLegacy; var itsOffsetMs = hlsVideoRequest == null ? 0 diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 1f6bc242df..fdddc0c37f 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -13,6 +13,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using ServiceStack; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -29,27 +30,60 @@ namespace MediaBrowser.Api.Playback.Hls /// [Route("/Videos/{Id}/master.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")] [Route("/Videos/{Id}/master.m3u8", "HEAD", Summary = "Gets a video stream using HTTP live streaming.")] - public class GetMasterHlsVideoStream : VideoStreamRequest + public class GetMasterHlsVideoPlaylist : VideoStreamRequest, IMasterHlsRequest { public bool EnableAdaptiveBitrateStreaming { get; set; } - public GetMasterHlsVideoStream() + public GetMasterHlsVideoPlaylist() { EnableAdaptiveBitrateStreaming = true; } } + [Route("/Audio/{Id}/master.m3u8", "GET", Summary = "Gets an audio stream using HTTP live streaming.")] + [Route("/Audio/{Id}/master.m3u8", "HEAD", Summary = "Gets an audio stream using HTTP live streaming.")] + public class GetMasterHlsAudioPlaylist : StreamRequest, IMasterHlsRequest + { + public bool EnableAdaptiveBitrateStreaming { get; set; } + + public GetMasterHlsAudioPlaylist() + { + EnableAdaptiveBitrateStreaming = true; + } + } + + public interface IMasterHlsRequest + { + bool EnableAdaptiveBitrateStreaming { get; set; } + } + [Route("/Videos/{Id}/main.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")] - public class GetMainHlsVideoStream : VideoStreamRequest + public class GetVariantHlsVideoPlaylist : VideoStreamRequest + { + } + + [Route("/Audio/{Id}/main.m3u8", "GET", Summary = "Gets an audio stream using HTTP live streaming.")] + public class GetVariantHlsAudioPlaylist : StreamRequest { } - /// - /// Class GetHlsVideoSegment - /// [Route("/Videos/{Id}/hlsdynamic/{PlaylistId}/{SegmentId}.ts", "GET")] [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] - public class GetDynamicHlsVideoSegment : VideoStreamRequest + public class GetHlsVideoSegment : VideoStreamRequest + { + public string PlaylistId { get; set; } + + /// + /// Gets or sets the segment id. + /// + /// The segment id. + public string SegmentId { get; set; } + } + + [Route("/Audio/{Id}/hlsdynamic/{PlaylistId}/{SegmentId}.aac", "GET")] + [Route("/Audio/{Id}/hlsdynamic/{PlaylistId}/{SegmentId}.ts", "GET")] + [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] + public class GetHlsAudioSegment : StreamRequest { public string PlaylistId { get; set; } @@ -62,34 +96,55 @@ namespace MediaBrowser.Api.Playback.Hls public class DynamicHlsService : BaseHlsService { - public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer) + public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, INetworkManager networkManager) + : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer) { NetworkManager = networkManager; } protected INetworkManager NetworkManager { get; private set; } - public Task Get(GetMasterHlsVideoStream request) + public Task Get(GetMasterHlsVideoPlaylist request) + { + return GetMasterPlaylistInternal(request, "GET"); + } + + public Task Head(GetMasterHlsVideoPlaylist request) { - return GetAsync(request, "GET"); + return GetMasterPlaylistInternal(request, "HEAD"); } - public Task Head(GetMasterHlsVideoStream request) + public Task Get(GetMasterHlsAudioPlaylist request) { - return GetAsync(request, "HEAD"); + return GetMasterPlaylistInternal(request, "GET"); } - public Task Get(GetMainHlsVideoStream request) + public Task Head(GetMasterHlsAudioPlaylist request) { - return GetPlaylistAsync(request, "main"); + return GetMasterPlaylistInternal(request, "HEAD"); + } + + public Task Get(GetVariantHlsVideoPlaylist request) + { + return GetVariantPlaylistInternal(request, true, "main"); + } + + public Task Get(GetVariantHlsAudioPlaylist request) + { + return GetVariantPlaylistInternal(request, false, "main"); + } + + public Task Get(GetHlsVideoSegment request) + { + return GetDynamicSegment(request, request.SegmentId); } - public Task Get(GetDynamicHlsVideoSegment request) + public Task Get(GetHlsAudioSegment request) { return GetDynamicSegment(request, request.SegmentId); } - private async Task GetDynamicSegment(VideoStreamRequest request, string segmentId) + private async Task GetDynamicSegment(StreamRequest request, string segmentId) { if ((request.StartTimeTicks ?? 0) > 0) { @@ -105,7 +160,7 @@ namespace MediaBrowser.Api.Playback.Hls var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8"); - var segmentPath = GetSegmentPath(playlistPath, requestedIndex); + var segmentPath = GetSegmentPath(state, playlistPath, requestedIndex); var segmentLength = state.SegmentLength; var segmentExtension = GetSegmentFileExtension(state); @@ -130,7 +185,7 @@ namespace MediaBrowser.Api.Playback.Hls { var startTranscoding = false; - var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); + var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, request.PlaySessionId, segmentExtension); var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength; if (currentTranscodingIndex == null) @@ -155,12 +210,14 @@ namespace MediaBrowser.Api.Playback.Hls { ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.PlaySessionId, p => false); + await ReadSegmentLengths(playlistPath).ConfigureAwait(false); + if (currentTranscodingIndex.HasValue) { DeleteLastFile(playlistPath, segmentExtension, 0); } - request.StartTimeTicks = GetSeekPositionTicks(state, requestedIndex); + request.StartTimeTicks = GetSeekPositionTicks(state, playlistPath, requestedIndex); job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false); } @@ -187,28 +244,92 @@ namespace MediaBrowser.Api.Playback.Hls ApiEntryPoint.Instance.TranscodingStartLock.Release(); } - Logger.Info("waiting for {0}", segmentPath); - while (!File.Exists(segmentPath)) - { - await Task.Delay(50, cancellationToken).ConfigureAwait(false); - } + //Logger.Info("waiting for {0}", segmentPath); + //while (!File.Exists(segmentPath)) + //{ + // await Task.Delay(50, cancellationToken).ConfigureAwait(false); + //} Logger.Info("returning {0}", segmentPath); job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false); } - private long GetSeekPositionTicks(StreamState state, int requestedIndex) + private static readonly ConcurrentDictionary SegmentLengths = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + private async Task ReadSegmentLengths(string playlist) { - var startSeconds = requestedIndex * state.SegmentLength; - var position = TimeSpan.FromSeconds(startSeconds).Ticks; + try + { + using (var fileStream = GetPlaylistFileStream(playlist)) + { + using (var reader = new StreamReader(fileStream)) + { + double duration = -1; + + while (!reader.EndOfStream) + { + var text = await reader.ReadLineAsync().ConfigureAwait(false); + + if (text.StartsWith("#EXTINF", StringComparison.OrdinalIgnoreCase)) + { + var parts = text.Split(new[] { ':' }, 2); + if (parts.Length == 2) + { + var time = parts[1].Trim(new[] { ',' }).Trim(); + double timeValue; + if (double.TryParse(time, NumberStyles.Any, CultureInfo.InvariantCulture, out timeValue)) + { + duration = timeValue; + continue; + } + } + } + else if (duration != -1) + { + SegmentLengths.AddOrUpdate(text, duration, (k, v) => duration); + Logger.Debug("Added segment length of {0} for {1}", duration, text); + } + + duration = -1; + } + } + } + } + catch (FileNotFoundException) + { + + } + } + + private long GetSeekPositionTicks(StreamState state, string playlist, int requestedIndex) + { + double startSeconds = 0; + + for (var i = 0; i < requestedIndex; i++) + { + var segmentPath = GetSegmentPath(state, playlist, i); + double length; + if (SegmentLengths.TryGetValue(Path.GetFileName(segmentPath), out length)) + { + Logger.Debug("Found segment length of {0} for index {1}", length, i); + startSeconds += length; + } + else + { + startSeconds += state.SegmentLength; + } + } + + var position = TimeSpan.FromSeconds(startSeconds).Ticks; return position; } - public int? GetCurrentTranscodingIndex(string playlist, string segmentExtension) + public int? GetCurrentTranscodingIndex(string playlist, string playSessionId, string segmentExtension) { - var job = ApiEntryPoint.Instance.GetTranscodingJob(playlist, TranscodingJobType); + var job = string.IsNullOrWhiteSpace(playSessionId) ? + ApiEntryPoint.Instance.GetTranscodingJob(playlist, TranscodingJobType) : + ApiEntryPoint.Instance.GetTranscodingJobByPlaySessionId(playSessionId); if (job == null || job.HasExited) { @@ -292,7 +413,7 @@ namespace MediaBrowser.Api.Playback.Hls { var segmentId = "0"; - var segmentRequest = request as GetDynamicHlsVideoSegment; + var segmentRequest = request as GetHlsVideoSegment; if (segmentRequest != null) { segmentId = segmentRequest.SegmentId; @@ -301,13 +422,13 @@ namespace MediaBrowser.Api.Playback.Hls return int.Parse(segmentId, NumberStyles.Integer, UsCulture); } - private string GetSegmentPath(string playlist, int index) + private string GetSegmentPath(StreamState state, string playlist, int index) { var folder = Path.GetDirectoryName(playlist); var filename = Path.GetFileNameWithoutExtension(playlist); - return Path.Combine(folder, filename + index.ToString(UsCulture) + ".ts"); + return Path.Combine(folder, filename + index.ToString(UsCulture) + GetSegmentFileExtension(state)); } private async Task GetSegmentResult(string playlistPath, @@ -325,21 +446,26 @@ namespace MediaBrowser.Api.Playback.Hls var segmentFilename = Path.GetFileName(segmentPath); - using (var fileStream = GetPlaylistFileStream(playlistPath)) + while (!cancellationToken.IsCancellationRequested) { - using (var reader = new StreamReader(fileStream)) + using (var fileStream = GetPlaylistFileStream(playlistPath)) { - while (!reader.EndOfStream) + using (var reader = new StreamReader(fileStream)) { - var text = await reader.ReadLineAsync().ConfigureAwait(false); - - // If it appears in the playlist, it's done - if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1) + while (!reader.EndOfStream) { - return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob); + var text = await reader.ReadLineAsync().ConfigureAwait(false); + + // If it appears in the playlist, it's done + if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1) + { + return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob); + } } } } + + await Task.Delay(100, cancellationToken).ConfigureAwait(false); } // if a different file is encoding, it's done @@ -349,34 +475,35 @@ namespace MediaBrowser.Api.Playback.Hls //return GetSegmentResult(segmentPath, segmentIndex); //} - // Wait for the file to stop being written to, then stream it - var length = new FileInfo(segmentPath).Length; - var eofCount = 0; - - while (eofCount < 10) - { - var info = new FileInfo(segmentPath); - - if (!info.Exists) - { - break; - } - - var newLength = info.Length; + //// Wait for the file to stop being written to, then stream it + //var length = new FileInfo(segmentPath).Length; + //var eofCount = 0; - if (newLength == length) - { - eofCount++; - } - else - { - eofCount = 0; - } - - length = newLength; - await Task.Delay(100, cancellationToken).ConfigureAwait(false); - } + //while (eofCount < 10) + //{ + // var info = new FileInfo(segmentPath); + + // if (!info.Exists) + // { + // break; + // } + + // var newLength = info.Length; + + // if (newLength == length) + // { + // eofCount++; + // } + // else + // { + // eofCount = 0; + // } + + // length = newLength; + // await Task.Delay(100, cancellationToken).ConfigureAwait(false); + //} + cancellationToken.ThrowIfCancellationRequested(); return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob); } @@ -400,7 +527,7 @@ namespace MediaBrowser.Api.Playback.Hls }); } - private async Task GetAsync(GetMasterHlsVideoStream request, string method) + private async Task GetMasterPlaylistInternal(StreamRequest request, string method) { var state = await GetState(request, CancellationToken.None).ConfigureAwait(false); @@ -437,14 +564,16 @@ namespace MediaBrowser.Api.Playback.Hls var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8"; playlistUrl += queryString; - var request = (GetMasterHlsVideoStream)state.Request; + var request = state.Request; var subtitleStreams = state.MediaSource .MediaStreams .Where(i => i.IsTextSubtitleStream) .ToList(); - var subtitleGroup = subtitleStreams.Count > 0 && request.SubtitleMethod == SubtitleDeliveryMethod.Hls ? + var subtitleGroup = subtitleStreams.Count > 0 && + (request is GetMasterHlsVideoPlaylist) && + ((GetMasterHlsVideoPlaylist)request).SubtitleMethod == SubtitleDeliveryMethod.Hls ? "subs" : null; @@ -452,7 +581,7 @@ namespace MediaBrowser.Api.Playback.Hls if (EnableAdaptiveBitrateStreaming(state, isLiveStream)) { - var requestedVideoBitrate = state.VideoRequest.VideoBitRate.Value; + var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0; // By default, vary by just 200k var variation = GetBitrateVariation(totalBitrate); @@ -522,7 +651,7 @@ namespace MediaBrowser.Api.Playback.Hls return false; } - var request = state.Request as GetMasterHlsVideoStream; + var request = state.Request as IMasterHlsRequest; if (request != null && !request.EnableAdaptiveBitrateStreaming) { return false; @@ -544,6 +673,11 @@ namespace MediaBrowser.Api.Playback.Hls return false; } + if (!state.IsOutputVideo) + { + return false; + } + // Having problems in android return false; //return state.VideoRequest.VideoBitRate.HasValue; @@ -599,7 +733,7 @@ namespace MediaBrowser.Api.Playback.Hls return variation; } - private async Task GetPlaylistAsync(VideoStreamRequest request, string name) + private async Task GetVariantPlaylistInternal(StreamRequest request, bool isOutputVideo, string name) { var state = await GetState(request, CancellationToken.None).ConfigureAwait(false); @@ -607,7 +741,7 @@ namespace MediaBrowser.Api.Playback.Hls builder.AppendLine("#EXTM3U"); builder.AppendLine("#EXT-X-VERSION:3"); - builder.AppendLine("#EXT-X-TARGETDURATION:" + state.SegmentLength.ToString(UsCulture)); + builder.AppendLine("#EXT-X-TARGETDURATION:" + (state.SegmentLength).ToString(UsCulture)); builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0"); var queryStringIndex = Request.RawUrl.IndexOf('?'); @@ -623,10 +757,11 @@ namespace MediaBrowser.Api.Playback.Hls builder.AppendLine("#EXTINF:" + length.ToString(UsCulture) + ","); - builder.AppendLine(string.Format("hlsdynamic/{0}/{1}.ts{2}", + builder.AppendLine(string.Format("hlsdynamic/{0}/{1}{2}{3}", name, index.ToString(UsCulture), + GetSegmentFileExtension(isOutputVideo), queryString)); seconds -= state.SegmentLength; @@ -642,6 +777,28 @@ namespace MediaBrowser.Api.Playback.Hls protected override string GetAudioArguments(StreamState state) { + if (!state.IsOutputVideo) + { + var audioTranscodeParams = new List(); + if (state.OutputAudioBitrate.HasValue) + { + audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(UsCulture)); + } + + if (state.OutputAudioChannels.HasValue) + { + audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(UsCulture)); + } + + if (state.OutputAudioSampleRate.HasValue) + { + audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(UsCulture)); + } + + audioTranscodeParams.Add("-vn"); + return string.Join(" ", audioTranscodeParams.ToArray()); + } + var codec = state.OutputAudioCodec; if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) @@ -672,6 +829,11 @@ namespace MediaBrowser.Api.Playback.Hls protected override string GetVideoArguments(StreamState state) { + if (!state.IsOutputVideo) + { + return string.Empty; + } + var codec = state.OutputVideoCodec; var args = "-codec:v:0 " + codec; @@ -684,30 +846,36 @@ namespace MediaBrowser.Api.Playback.Hls // See if we can save come cpu cycles by avoiding encoding if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase)) { - return state.VideoStream != null && IsH264(state.VideoStream) ? - args + " -bsf:v h264_mp4toannexb" : - args; + args += state.VideoStream != null && IsH264(state.VideoStream) + ? args + " -bsf:v h264_mp4toannexb" + : args; } + else + { + var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})", + state.SegmentLength.ToString(UsCulture)); - var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})", - state.SegmentLength.ToString(UsCulture)); + var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream; - var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream; + args += " " + GetVideoQualityParam(state, H264Encoder, true) + keyFrameArg; - args += " " + GetVideoQualityParam(state, H264Encoder, true) + keyFrameArg; + //args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0"; - // Add resolution params, if specified - if (!hasGraphicalSubs) - { - args += GetOutputSizeParam(state, codec, false); - } + // Add resolution params, if specified + if (!hasGraphicalSubs) + { + args += GetOutputSizeParam(state, codec, false); + } - // This is for internal graphical subs - if (hasGraphicalSubs) - { - args += GetGraphicalSubtitleParam(state, codec); + // This is for internal graphical subs + if (hasGraphicalSubs) + { + args += GetGraphicalSubtitleParam(state, codec); + } } + args += " -flags +loop-global_header -sc_threshold 0"; + return args; } @@ -715,43 +883,96 @@ namespace MediaBrowser.Api.Playback.Hls { var threads = GetNumberOfThreads(state, false); - var inputModifier = GetInputModifier(state); + var inputModifier = GetInputModifier(state, false); // If isEncoding is true we're actually starting ffmpeg var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0"; - if (state.EnableGenericHlsSegmenter) + var toTimeParam = string.Empty; + if (EnableSplitTranscoding(state)) { - var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d.ts"; + var startTime = state.Request.StartTimeTicks ?? 0; + var durationSeconds = ApiEntryPoint.Instance.GetEncodingOptions().ThrottleThresholdInSeconds; + + var endTime = startTime + TimeSpan.FromSeconds(durationSeconds).Ticks; + endTime = Math.Min(endTime, state.RunTimeTicks.Value); + + if (endTime < state.RunTimeTicks.Value) + { + //toTimeParam = " -to " + MediaEncoder.GetTimeParameter(endTime); + toTimeParam = " -t " + MediaEncoder.GetTimeParameter(TimeSpan.FromSeconds(durationSeconds).Ticks); + } + } - return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -flags -global_header -sc_threshold 0 {5} -f segment -segment_time {6} -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"", - inputModifier, - GetInputArgument(state), - threads, - GetMapArgs(state), - GetVideoArguments(state), - GetAudioArguments(state), - state.SegmentLength.ToString(UsCulture), - startNumberParam, - outputPath, - outputTsArg - ).Trim(); + var timestampOffsetParam = string.Empty; + if (state.IsOutputVideo) + { + timestampOffsetParam = " -output_ts_offset " + MediaEncoder.GetTimeParameter(state.Request.StartTimeTicks ?? 0).ToString(CultureInfo.InvariantCulture); } + + var mapArgs = state.IsOutputVideo ? GetMapArgs(state) : string.Empty; + + //var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state); - return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -flags -global_header -copyts -sc_threshold 0 {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"", + //return string.Format("{0} {11} {1}{10} -map_metadata -1 -threads {2} {3} {4} {5} -f segment -segment_time {6} -segment_format mpegts -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"", + // inputModifier, + // GetInputArgument(state), + // threads, + // mapArgs, + // GetVideoArguments(state), + // GetAudioArguments(state), + // state.SegmentLength.ToString(UsCulture), + // startNumberParam, + // outputPath, + // outputTsArg, + // slowSeekParam, + // toTimeParam + // ).Trim(); + + return string.Format("{0}{11} {1} -map_metadata -1 -threads {2} {3} {4}{5} {6} -hls_time {7} -start_number {8} -hls_list_size {9} -y \"{10}\"", inputModifier, GetInputArgument(state), threads, - GetMapArgs(state), + mapArgs, GetVideoArguments(state), + timestampOffsetParam, GetAudioArguments(state), state.SegmentLength.ToString(UsCulture), startNumberParam, state.HlsListSize.ToString(UsCulture), - outputPath + outputPath, + toTimeParam ).Trim(); } + protected override bool EnableThrottling(StreamState state) + { + return !EnableSplitTranscoding(state); + } + + private bool EnableSplitTranscoding(StreamState state) + { + if (string.Equals(Request.QueryString["EnableSplitTranscoding"], "false", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return state.RunTimeTicks.HasValue && state.IsOutputVideo; + } + + protected override bool EnableStreamCopy + { + get + { + return false; + } + } + /// /// Gets the segment file extension. /// @@ -759,7 +980,12 @@ namespace MediaBrowser.Api.Playback.Hls /// System.String. protected override string GetSegmentFileExtension(StreamState state) { - return ".ts"; + return GetSegmentFileExtension(state.IsOutputVideo); + } + + protected string GetSegmentFileExtension(bool isOutputVideo) + { + return isOutputVideo ? ".ts" : ".ts"; } } } diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs index 5d8c67abe9..b44d7f6606 100644 --- a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs +++ b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs @@ -14,8 +14,10 @@ namespace MediaBrowser.Api.Playback.Hls [Route("/Audio/{Id}/hls/{SegmentId}/stream.mp3", "GET")] [Route("/Audio/{Id}/hls/{SegmentId}/stream.aac", "GET")] [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] - public class GetHlsAudioSegment + public class GetHlsAudioSegmentLegacy { + // TODO: Deprecate with new iOS app + /// /// Gets or sets the id. /// @@ -29,12 +31,31 @@ namespace MediaBrowser.Api.Playback.Hls public string SegmentId { get; set; } } + /// + /// Class GetHlsVideoStream + /// + [Route("/Videos/{Id}/stream.m3u8", "GET")] + [Api(Description = "Gets a video stream using HTTP live streaming.")] + public class GetHlsVideoStreamLegacy : VideoStreamRequest + { + // TODO: Deprecate with new iOS app + + [ApiMember(Name = "BaselineStreamAudioBitRate", Description = "Optional. Specify the audio bitrate for the baseline stream.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? BaselineStreamAudioBitRate { get; set; } + + [ApiMember(Name = "AppendBaselineStream", Description = "Optional. Whether or not to include a baseline audio-only stream in the master playlist.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool AppendBaselineStream { get; set; } + + [ApiMember(Name = "TimeStampOffsetMs", Description = "Optional. Alter the timestamps in the playlist by a given amount, in ms. Default is 1000.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int TimeStampOffsetMs { get; set; } + } + /// /// Class GetHlsVideoSegment /// [Route("/Videos/{Id}/hls/{PlaylistId}/stream.m3u8", "GET")] [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] - public class GetHlsPlaylist + public class GetHlsPlaylistLegacy { // TODO: Deprecate with new iOS app @@ -63,8 +84,10 @@ namespace MediaBrowser.Api.Playback.Hls /// [Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.ts", "GET")] [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] - public class GetHlsVideoSegment : VideoStreamRequest + public class GetHlsVideoSegmentLegacy : VideoStreamRequest { + // TODO: Deprecate with new iOS app + public string PlaylistId { get; set; } /// @@ -85,7 +108,7 @@ namespace MediaBrowser.Api.Playback.Hls _config = config; } - public object Get(GetHlsPlaylist request) + public object Get(GetHlsPlaylistLegacy request) { var file = request.PlaylistId + Path.GetExtension(Request.PathInfo); file = Path.Combine(_appPaths.TranscodingTempPath, file); @@ -103,7 +126,7 @@ namespace MediaBrowser.Api.Playback.Hls /// /// The request. /// System.Object. - public object Get(GetHlsVideoSegment request) + public object Get(GetHlsVideoSegmentLegacy request) { var file = request.SegmentId + Path.GetExtension(Request.PathInfo); file = Path.Combine(_config.ApplicationPaths.TranscodingTempPath, file); @@ -121,7 +144,7 @@ namespace MediaBrowser.Api.Playback.Hls /// /// The request. /// System.Object. - public object Get(GetHlsAudioSegment request) + public object Get(GetHlsAudioSegmentLegacy request) { // TODO: Deprecate with new iOS app var file = request.SegmentId + Path.GetExtension(Request.PathInfo); diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index 626df59f25..f21be190fe 100644 --- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs @@ -11,25 +11,6 @@ using System; namespace MediaBrowser.Api.Playback.Hls { - /// - /// Class GetHlsVideoStream - /// - [Route("/Videos/{Id}/stream.m3u8", "GET")] - [Api(Description = "Gets a video stream using HTTP live streaming.")] - public class GetHlsVideoStream : VideoStreamRequest - { - // TODO: Deprecate with new iOS app - - [ApiMember(Name = "BaselineStreamAudioBitRate", Description = "Optional. Specify the audio bitrate for the baseline stream.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? BaselineStreamAudioBitRate { get; set; } - - [ApiMember(Name = "AppendBaselineStream", Description = "Optional. Whether or not to include a baseline audio-only stream in the master playlist.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool AppendBaselineStream { get; set; } - - [ApiMember(Name = "TimeStampOffsetMs", Description = "Optional. Alter the timestamps in the playlist by a given amount, in ms. Default is 1000.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int TimeStampOffsetMs { get; set; } - } - [Route("/Videos/{Id}/live.m3u8", "GET")] [Api(Description = "Gets a video stream using HTTP live streaming.")] public class GetLiveHlsStream : VideoStreamRequest @@ -50,7 +31,7 @@ namespace MediaBrowser.Api.Playback.Hls /// /// The request. /// System.Object. - public object Get(GetHlsVideoStream request) + public object Get(GetHlsVideoStreamLegacy request) { return ProcessRequest(request, false); } diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index 27482c50c6..283f9671fa 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -15,7 +15,7 @@ using System.IO; namespace MediaBrowser.Api.Playback.Progressive { /// - /// Class GetAudioStream + /// Class GetVideoStream /// [Route("/Videos/{Id}/stream.ts", "GET")] [Route("/Videos/{Id}/stream.webm", "GET")] diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index 2d1e896db0..02b7720a4a 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/MediaBrowser.Api/Playback/StreamState.cs @@ -41,7 +41,7 @@ namespace MediaBrowser.Api.Playback public string InputContainer { get; set; } public MediaSourceInfo MediaSource { get; set; } - + public MediaStream AudioStream { get; set; } public MediaStream VideoStream { get; set; } public MediaStream SubtitleStream { get; set; } @@ -57,6 +57,10 @@ namespace MediaBrowser.Api.Playback public MediaProtocol InputProtocol { get; set; } + public bool IsOutputVideo + { + get { return Request is VideoStreamRequest; } + } public bool IsInputVideo { get; set; } public bool IsInputArchive { get; set; } @@ -66,7 +70,6 @@ namespace MediaBrowser.Api.Playback public List PlayableStreamFileNames { get; set; } public int SegmentLength = 3; - public bool EnableGenericHlsSegmenter = false; public int HlsListSize { get diff --git a/MediaBrowser.Api/Playback/TranscodingThrottler.cs b/MediaBrowser.Api/Playback/TranscodingThrottler.cs index ece4550095..fec3dda869 100644 --- a/MediaBrowser.Api/Playback/TranscodingThrottler.cs +++ b/MediaBrowser.Api/Playback/TranscodingThrottler.cs @@ -42,7 +42,7 @@ namespace MediaBrowser.Api.Playback var options = GetOptions(); - if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleThresholdSeconds)) + if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleThresholdInSeconds)) { PauseTranscoding(); } diff --git a/MediaBrowser.Api/PluginService.cs b/MediaBrowser.Api/PluginService.cs index 4bd78f1f55..4af9bfe58a 100644 --- a/MediaBrowser.Api/PluginService.cs +++ b/MediaBrowser.Api/PluginService.cs @@ -1,7 +1,9 @@ using MediaBrowser.Common; using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; using MediaBrowser.Common.Security; using MediaBrowser.Common.Updates; +using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Plugins; @@ -25,6 +27,7 @@ namespace MediaBrowser.Api [Authenticated] public class GetPlugins : IReturn> { + public bool? IsAppStoreEnabled { get; set; } } /// @@ -133,8 +136,10 @@ namespace MediaBrowser.Api private readonly ISecurityManager _securityManager; private readonly IInstallationManager _installationManager; + private readonly INetworkManager _network; + private readonly IDeviceManager _deviceManager; - public PluginService(IJsonSerializer jsonSerializer, IApplicationHost appHost, ISecurityManager securityManager, IInstallationManager installationManager) + public PluginService(IJsonSerializer jsonSerializer, IApplicationHost appHost, ISecurityManager securityManager, IInstallationManager installationManager, INetworkManager network, IDeviceManager deviceManager) : base() { if (jsonSerializer == null) @@ -145,6 +150,8 @@ namespace MediaBrowser.Api _appHost = appHost; _securityManager = securityManager; _installationManager = installationManager; + _network = network; + _deviceManager = deviceManager; _jsonSerializer = jsonSerializer; } @@ -164,13 +171,15 @@ namespace MediaBrowser.Api { var result = await _securityManager.GetRegistrationStatus(request.Name).ConfigureAwait(false); - return ToOptimizedResult(new RegistrationInfo + var info = new RegistrationInfo { ExpirationDate = result.ExpirationDate, IsRegistered = result.IsRegistered, IsTrial = result.TrialVersion, Name = request.Name - }); + }; + + return ToOptimizedResult(info); } /// @@ -181,6 +190,7 @@ namespace MediaBrowser.Api public async Task Get(GetPlugins request) { var result = _appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()).ToList(); + var requireAppStoreEnabled = request.IsAppStoreEnabled.HasValue && request.IsAppStoreEnabled.Value; // Don't fail just on account of image url's try @@ -197,10 +207,26 @@ namespace MediaBrowser.Api plugin.ImageUrl = pkg.thumbImage; } } + + if (requireAppStoreEnabled) + { + result = result + .Where(plugin => + { + var pkg = packages.FirstOrDefault(i => !string.IsNullOrWhiteSpace(i.guid) && new Guid(plugin.Id).Equals(new Guid(i.guid))); + return pkg != null && pkg.enableInAppStore; + + }) + .ToList(); + } } catch { - + // Play it safe here + if (requireAppStoreEnabled) + { + result = new List(); + } } return ToOptimizedSerializedResultUsingCache(result); diff --git a/MediaBrowser.Api/Reports/Common/HeaderMetadata.cs b/MediaBrowser.Api/Reports/Common/HeaderMetadata.cs new file mode 100644 index 0000000000..3cb8f722d2 --- /dev/null +++ b/MediaBrowser.Api/Reports/Common/HeaderMetadata.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Api.Reports +{ + public enum HeaderMetadata + { + None, + Name, + PremiereDate, + DateAdded, + ReleaseDate, + Runtime, + PlayCount, + Season, + SeasonNumber, + Series, + Network, + Year, + ParentalRating, + CommunityRating, + Trailers, + Specials, + GameSystem, + Players, + AlbumArtist, + Album, + Disc, + Track, + Audio, + EmbeddedImage, + Video, + Resolution, + Subtitles, + Genres, + Countries, + StatusImage, + Tracks, + EpisodeSeries, + EpisodeSeason, + AudioAlbumArtist, + MusicArtist, + AudioAlbum, + Status + } +} diff --git a/MediaBrowser.Api/Reports/Common/ItemViewType.cs b/MediaBrowser.Api/Reports/Common/ItemViewType.cs new file mode 100644 index 0000000000..3e09a290dc --- /dev/null +++ b/MediaBrowser.Api/Reports/Common/ItemViewType.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Api.Reports +{ + public enum ItemViewType + { + None, + Detail, + Edit, + List, + ItemByNameDetails, + StatusImage, + EmbeddedImage, + SubtitleImage, + TrailersImage, + SpecialsImage + } +} diff --git a/MediaBrowser.Api/Reports/Common/ReportBuilderBase.cs b/MediaBrowser.Api/Reports/Common/ReportBuilderBase.cs new file mode 100644 index 0000000000..af6dc997c2 --- /dev/null +++ b/MediaBrowser.Api/Reports/Common/ReportBuilderBase.cs @@ -0,0 +1,229 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Channels; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.Reports +{ + /// A report builder base. + public class ReportBuilderBase + { + /// + /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportBuilderBase class. + /// Manager for library. + public ReportBuilderBase(ILibraryManager libraryManager) + { + _libraryManager = libraryManager; + } + + /// Manager for library. + protected readonly ILibraryManager _libraryManager; + + /// Gets audio stream. + /// The item. + /// The audio stream. + protected string GetAudioStream(BaseItem item) + { + var stream = GetStream(item, MediaStreamType.Audio); + if (stream != null) + return stream.Codec.ToUpper() == "DCA" ? stream.Profile : stream.Codec. + ToUpper(); + + return string.Empty; + } + + /// Gets an episode. + /// The item. + /// The episode. + protected string GetEpisode(BaseItem item) + { + + if (item.GetClientTypeName() == ChannelMediaContentType.Episode.ToString() && item.ParentIndexNumber != null) + return "Season " + item.ParentIndexNumber; + else + return item.Name; + } + + /// Gets a genre. + /// The name. + /// The genre. + protected Genre GetGenre(string name) + { + if (string.IsNullOrEmpty(name)) + return null; + return _libraryManager.GetGenre(name); + } + + /// Gets genre identifier. + /// The name. + /// The genre identifier. + protected string GetGenreID(string name) + { + if (string.IsNullOrEmpty(name)) + return string.Empty; + return string.Format("{0:N}", + GetGenre(name).Id); + } + + /// Gets list as string. + /// The items. + /// The list as string. + protected string GetListAsString(List items) + { + return String.Join("; ", items); + } + + /// Gets media source information. + /// The item. + /// The media source information. + protected MediaSourceInfo GetMediaSourceInfo(BaseItem item) + { + var mediaSource = item as IHasMediaSources; + if (mediaSource != null) + return mediaSource.GetMediaSources(false).FirstOrDefault(n => n.Type == MediaSourceType.Default); + + return null; + } + + /// Gets an object. + /// Generic type parameter. + /// Type of the r. + /// The item. + /// The function. + /// The default value. + /// The object. + protected R GetObject(BaseItem item, Func function, R defaultValue = default(R)) where T : class + { + var value = item as T; + if (value != null && function != null) + return function(value); + else + return defaultValue; + } + + /// Gets a person. + /// The name. + /// The person. + protected Person GetPerson(string name) + { + if (string.IsNullOrEmpty(name)) + return null; + return _libraryManager.GetPerson(name); + } + + /// Gets person identifier. + /// The name. + /// The person identifier. + protected string GetPersonID(string name) + { + if (string.IsNullOrEmpty(name)) + return string.Empty; + return string.Format("{0:N}", + GetPerson(name).Id); + } + + /// Gets runtime date time. + /// The runtime. + /// The runtime date time. + protected double? GetRuntimeDateTime(long? runtime) + { + if (runtime.HasValue) + return Math.Ceiling(new TimeSpan(runtime.Value).TotalMinutes); + return null; + } + + /// Gets series production year. + /// The item. + /// The series production year. + protected string GetSeriesProductionYear(BaseItem item) + { + + string productionYear = item.ProductionYear.ToString(); + var series = item as Series; + if (series == null) + { + if (item.ProductionYear == null || item.ProductionYear == 0) + return string.Empty; + return productionYear; + } + + if (series.Status == SeriesStatus.Continuing) + return productionYear += "-Present"; + + if (series.EndDate != null && series.EndDate.Value.Year != series.ProductionYear) + return productionYear += "-" + series.EndDate.Value.Year; + + return productionYear; + } + + /// Gets a stream. + /// The item. + /// Type of the stream. + /// The stream. + protected MediaStream GetStream(BaseItem item, MediaStreamType streamType) + { + var itemInfo = GetMediaSourceInfo(item); + if (itemInfo != null) + return itemInfo.MediaStreams.FirstOrDefault(n => n.Type == streamType); + + return null; + } + + /// Gets a studio. + /// The name. + /// The studio. + protected Studio GetStudio(string name) + { + if (string.IsNullOrEmpty(name)) + return null; + return _libraryManager.GetStudio(name); + } + + /// Gets studio identifier. + /// The name. + /// The studio identifier. + protected string GetStudioID(string name) + { + if (string.IsNullOrEmpty(name)) + return string.Empty; + return string.Format("{0:N}", + GetStudio(name).Id); + } + + /// Gets video resolution. + /// The item. + /// The video resolution. + protected string GetVideoResolution(BaseItem item) + { + var stream = GetStream(item, + MediaStreamType.Video); + if (stream != null && stream.Width != null) + return string.Format("{0} * {1}", + stream.Width, + (stream.Height != null ? stream.Height.ToString() : "-")); + + return string.Empty; + } + + /// Gets video stream. + /// The item. + /// The video stream. + protected string GetVideoStream(BaseItem item) + { + var stream = GetStream(item, MediaStreamType.Video); + if (stream != null) + return stream.Codec.ToUpper(); + + return string.Empty; + } + + } +} diff --git a/MediaBrowser.Api/Reports/Common/ReportExportType.cs b/MediaBrowser.Api/Reports/Common/ReportExportType.cs new file mode 100644 index 0000000000..05f27f72ef --- /dev/null +++ b/MediaBrowser.Api/Reports/Common/ReportExportType.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Api.Reports +{ + public enum ReportExportType + { + CSV, + Excel + } +} diff --git a/MediaBrowser.Api/Reports/Common/ReportFieldType.cs b/MediaBrowser.Api/Reports/Common/ReportFieldType.cs new file mode 100644 index 0000000000..58523657aa --- /dev/null +++ b/MediaBrowser.Api/Reports/Common/ReportFieldType.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Api.Reports +{ + public enum ReportFieldType + { + String, + Boolean, + Date, + Time, + DateTime, + Int, + Image, + Object, + Minutes + } +} diff --git a/MediaBrowser.Api/Reports/Common/ReportHeaderIdType.cs b/MediaBrowser.Api/Reports/Common/ReportHeaderIdType.cs new file mode 100644 index 0000000000..58c1181510 --- /dev/null +++ b/MediaBrowser.Api/Reports/Common/ReportHeaderIdType.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Api.Reports +{ + public enum ReportHeaderIdType + { + Row, + Item + } +} diff --git a/MediaBrowser.Api/Reports/Common/ReportHelper.cs b/MediaBrowser.Api/Reports/Common/ReportHelper.cs new file mode 100644 index 0000000000..a557248c61 --- /dev/null +++ b/MediaBrowser.Api/Reports/Common/ReportHelper.cs @@ -0,0 +1,101 @@ +using MediaBrowser.Controller.Entities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.Reports +{ + public class ReportHelper + { + /// Gets java script localized string. + /// The phrase. + /// The java script localized string. + public static string GetJavaScriptLocalizedString(string phrase) + { + var dictionary = BaseItem.LocalizationManager.GetJavaScriptLocalizationDictionary(BaseItem.ConfigurationManager.Configuration.UICulture); + + string value; + + if (dictionary.TryGetValue(phrase, out value)) + { + return value; + } + + return phrase; + } + + /// Gets server localized string. + /// The phrase. + /// The server localized string. + public static string GetServerLocalizedString(string phrase) + { + return BaseItem.LocalizationManager.GetLocalizedString(phrase, BaseItem.ConfigurationManager.Configuration.UICulture); + } + + /// Gets row type. + /// The type. + /// The row type. + public static ReportViewType GetRowType(string rowType) + { + if (string.IsNullOrEmpty(rowType)) + return ReportViewType.BaseItem; + + ReportViewType rType; + + if (!Enum.TryParse(rowType, out rType)) + return ReportViewType.BaseItem; + + return rType; + } + + /// Gets header metadata type. + /// The header. + /// The header metadata type. + public static HeaderMetadata GetHeaderMetadataType(string header) + { + if (string.IsNullOrEmpty(header)) + return HeaderMetadata.None; + + HeaderMetadata rType; + + if (!Enum.TryParse(header, out rType)) + return HeaderMetadata.None; + + return rType; + } + + /// Convert field to string. + /// Generic type parameter. + /// The value. + /// Type of the field. + /// The field converted to string. + public static string ConvertToString(T value, ReportFieldType fieldType) + { + if (value == null) + return ""; + switch (fieldType) + { + case ReportFieldType.String: + return value.ToString(); + case ReportFieldType.Boolean: + return value.ToString(); + case ReportFieldType.Date: + return string.Format("{0:d}", value); + case ReportFieldType.Time: + return string.Format("{0:t}", value); + case ReportFieldType.DateTime: + return string.Format("{0:d}", value); + case ReportFieldType.Minutes: + return string.Format("{0}mn", value); + case ReportFieldType.Int: + return string.Format("", value); + default: + if (value is Guid) + return string.Format("{0:N}", value); + return value.ToString(); + } + } + } +} diff --git a/MediaBrowser.Api/Reports/Common/ReportViewType.cs b/MediaBrowser.Api/Reports/Common/ReportViewType.cs new file mode 100644 index 0000000000..efdfcb0e79 --- /dev/null +++ b/MediaBrowser.Api/Reports/Common/ReportViewType.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Api.Reports +{ + public enum ReportViewType + { + MusicArtist, + MusicAlbum, + Book, + BoxSet, + Episode, + Game, + Video, + Movie, + MusicVideo, + Trailer, + Season, + Series, + Audio, + BaseItem, + Artist + } +} diff --git a/MediaBrowser.Api/Reports/Data/ReportBuilder.cs b/MediaBrowser.Api/Reports/Data/ReportBuilder.cs new file mode 100644 index 0000000000..00ce183178 --- /dev/null +++ b/MediaBrowser.Api/Reports/Data/ReportBuilder.cs @@ -0,0 +1,589 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Localization; +using MediaBrowser.Model.Channels; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.Reports +{ + /// A report builder. + /// + public class ReportBuilder : ReportBuilderBase + { + + /// + /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportBuilder class. + /// Manager for library. + public ReportBuilder(ILibraryManager libraryManager) + : base(libraryManager) + { + } + + private Func GetBoolString = s => s == true ? "x" : ""; + + public ReportResult GetReportResult(BaseItem[] items, ReportViewType reportRowType, BaseReportRequest request) + { + List headersMetadata = this.GetFilteredReportHeaderMetadata(reportRowType, request); + + var headers = GetReportHeaders(reportRowType, headersMetadata); + var rows = GetReportRows(items, headersMetadata); + + ReportResult result = new ReportResult { Headers = headers }; + HeaderMetadata groupBy = ReportHelper.GetHeaderMetadataType(request.GroupBy); + int i = headers.FindIndex(x => x.FieldName == groupBy); + if (groupBy != HeaderMetadata.None && i > 0) + { + var rowsGroup = rows.SelectMany(x => x.Columns[i].Name.Split(';'), (x, g) => new { Genre = g.Trim(), Rows = x }) + .GroupBy(x => x.Genre) + .OrderBy(x => x.Key) + .Select(x => new ReportGroup { Name = x.Key, Rows = x.Select(r => r.Rows).ToList() }); + + result.Groups = rowsGroup.ToList(); + result.IsGrouped = true; + } + else + { + result.Rows = rows; + result.IsGrouped = false; + } + + return result; + } + + public List GetReportHeaders(ReportViewType reportRowType, BaseReportRequest request) + { + List headersMetadata = this.GetReportHeaders(reportRowType); + if (request != null && !string.IsNullOrEmpty(request.ReportColumns)) + { + List headersMetadataFiltered = this.GetFilteredReportHeaderMetadata(reportRowType, request); + foreach (ReportHeader reportHeader in headersMetadata) + { + if (!headersMetadataFiltered.Contains(reportHeader.FieldName)) + { + reportHeader.Visible = false; + } + } + + + } + + return headersMetadata; + } + + public List GetReportHeaders(ReportViewType reportRowType, List headersMetadata = null) + { + if (headersMetadata == null) + headersMetadata = this.GetDefaultReportHeaderMetadata(reportRowType); + + List> options = new List>(); + foreach (HeaderMetadata header in headersMetadata) + { + options.Add(GetReportOption(header)); + } + + + List headers = new List(); + foreach (ReportOptions option in options) + { + headers.Add(option.Header); + } + return headers; + } + + private List GetReportRows(IEnumerable items, List headersMetadata) + { + List> options = new List>(); + foreach (HeaderMetadata header in headersMetadata) + { + options.Add(GetReportOption(header)); + } + + var rows = new List(); + + foreach (BaseItem item in items) + { + ReportRow rRow = GetRow(item); + foreach (ReportOptions option in options) + { + object itemColumn = option.Column != null ? option.Column(item, rRow) : ""; + object itemId = option.ItemID != null ? option.ItemID(item) : ""; + ReportItem rItem = new ReportItem + { + Name = ReportHelper.ConvertToString(itemColumn, option.Header.HeaderFieldType), + Id = ReportHelper.ConvertToString(itemId, ReportFieldType.Object) + }; + rRow.Columns.Add(rItem); + } + + rows.Add(rRow); + } + + return rows; + } + + /// Gets a row. + /// The item. + /// The row. + private ReportRow GetRow(BaseItem item) + { + var hasTrailers = item as IHasTrailers; + var hasSpecialFeatures = item as IHasSpecialFeatures; + var video = item as Video; + ReportRow rRow = new ReportRow + { + Id = item.Id.ToString("N"), + HasLockData = item.IsLocked, + IsUnidentified = item.IsUnidentified, + HasLocalTrailer = hasTrailers != null ? hasTrailers.GetTrailerIds().Count() > 0 : false, + HasImageTagsPrimary = (item.ImageInfos != null && item.ImageInfos.Count(n => n.Type == ImageType.Primary) > 0), + HasImageTagsBackdrop = (item.ImageInfos != null && item.ImageInfos.Count(n => n.Type == ImageType.Backdrop) > 0), + HasImageTagsLogo = (item.ImageInfos != null && item.ImageInfos.Count(n => n.Type == ImageType.Logo) > 0), + HasSpecials = hasSpecialFeatures != null ? hasSpecialFeatures.SpecialFeatureIds.Count > 0 : false, + HasSubtitles = video != null ? video.HasSubtitles : false, + RowType = ReportHelper.GetRowType(item.GetClientTypeName()) + }; + return rRow; + } + public List GetFilteredReportHeaderMetadata(ReportViewType reportRowType, BaseReportRequest request) + { + if (request != null && !string.IsNullOrEmpty(request.ReportColumns)) + { + var s = request.ReportColumns.Split('|').Select(x => ReportHelper.GetHeaderMetadataType(x)).Where(x => x != HeaderMetadata.None); + return s.ToList(); + } + else + return this.GetDefaultReportHeaderMetadata(reportRowType); + + } + + public List GetDefaultReportHeaderMetadata(ReportViewType reportRowType) + { + switch (reportRowType) + { + case ReportViewType.Season: + return new List + { + HeaderMetadata.StatusImage, + HeaderMetadata.Series, + HeaderMetadata.Season, + HeaderMetadata.SeasonNumber, + HeaderMetadata.DateAdded, + HeaderMetadata.Year, + HeaderMetadata.Genres + }; + + case ReportViewType.Series: + return new List + { + HeaderMetadata.StatusImage, + HeaderMetadata.Name, + HeaderMetadata.Network, + HeaderMetadata.DateAdded, + HeaderMetadata.Year, + HeaderMetadata.Genres, + HeaderMetadata.ParentalRating, + HeaderMetadata.CommunityRating, + HeaderMetadata.Runtime, + HeaderMetadata.Trailers, + HeaderMetadata.Specials + }; + + case ReportViewType.MusicAlbum: + return new List + { + HeaderMetadata.StatusImage, + HeaderMetadata.Name, + HeaderMetadata.AlbumArtist, + HeaderMetadata.DateAdded, + HeaderMetadata.ReleaseDate, + HeaderMetadata.Tracks, + HeaderMetadata.Year, + HeaderMetadata.Genres + }; + + case ReportViewType.MusicArtist: + return new List + { + HeaderMetadata.StatusImage, + HeaderMetadata.MusicArtist, + HeaderMetadata.Countries, + HeaderMetadata.DateAdded, + HeaderMetadata.Year, + HeaderMetadata.Genres + }; + + case ReportViewType.Game: + return new List + { + HeaderMetadata.StatusImage, + HeaderMetadata.Name, + HeaderMetadata.GameSystem, + HeaderMetadata.DateAdded, + HeaderMetadata.ReleaseDate, + HeaderMetadata.ParentalRating, + HeaderMetadata.CommunityRating, + HeaderMetadata.Players, + HeaderMetadata.Year, + HeaderMetadata.Genres, + HeaderMetadata.Trailers + }; + + case ReportViewType.Movie: + return new List + { + HeaderMetadata.StatusImage, + HeaderMetadata.Name, + HeaderMetadata.DateAdded, + HeaderMetadata.ReleaseDate, + HeaderMetadata.Year, + HeaderMetadata.Genres, + HeaderMetadata.ParentalRating, + HeaderMetadata.CommunityRating, + HeaderMetadata.Runtime, + HeaderMetadata.Video, + HeaderMetadata.Resolution, + HeaderMetadata.Audio, + HeaderMetadata.Subtitles, + HeaderMetadata.Trailers, + HeaderMetadata.Specials + }; + + case ReportViewType.Book: + return new List + { + HeaderMetadata.StatusImage, + HeaderMetadata.Name, + HeaderMetadata.DateAdded, + HeaderMetadata.ReleaseDate, + HeaderMetadata.Year, + HeaderMetadata.Genres, + HeaderMetadata.ParentalRating, + HeaderMetadata.CommunityRating + }; + + case ReportViewType.BoxSet: + return new List + { + HeaderMetadata.StatusImage, + HeaderMetadata.Name, + HeaderMetadata.DateAdded, + HeaderMetadata.ReleaseDate, + HeaderMetadata.Year, + HeaderMetadata.Genres, + HeaderMetadata.ParentalRating, + HeaderMetadata.CommunityRating, + HeaderMetadata.Trailers + }; + + case ReportViewType.Audio: + return new List + { + HeaderMetadata.StatusImage, + HeaderMetadata.Name, + HeaderMetadata.AudioAlbumArtist, + HeaderMetadata.AudioAlbum, + HeaderMetadata.Disc, + HeaderMetadata.Track, + HeaderMetadata.DateAdded, + HeaderMetadata.ReleaseDate, + HeaderMetadata.Year, + HeaderMetadata.Genres, + HeaderMetadata.ParentalRating, + HeaderMetadata.CommunityRating, + HeaderMetadata.Runtime, + HeaderMetadata.Audio + }; + + case ReportViewType.Episode: + return new List + { + HeaderMetadata.StatusImage, + HeaderMetadata.Name, + HeaderMetadata.EpisodeSeries, + HeaderMetadata.Season, + HeaderMetadata.DateAdded, + HeaderMetadata.ReleaseDate, + HeaderMetadata.Year, + HeaderMetadata.Genres, + HeaderMetadata.ParentalRating, + HeaderMetadata.CommunityRating, + HeaderMetadata.Runtime, + HeaderMetadata.Video, + HeaderMetadata.Resolution, + HeaderMetadata.Audio, + HeaderMetadata.Subtitles, + HeaderMetadata.Trailers, + HeaderMetadata.Specials + }; + + case ReportViewType.Video: + case ReportViewType.MusicVideo: + case ReportViewType.Trailer: + case ReportViewType.BaseItem: + default: + return new List + { + HeaderMetadata.StatusImage, + HeaderMetadata.Name, + HeaderMetadata.DateAdded, + HeaderMetadata.ReleaseDate, + HeaderMetadata.Year, + HeaderMetadata.Genres, + HeaderMetadata.ParentalRating, + HeaderMetadata.CommunityRating, + HeaderMetadata.Runtime, + HeaderMetadata.Video, + HeaderMetadata.Resolution, + HeaderMetadata.Audio, + HeaderMetadata.Subtitles, + HeaderMetadata.Trailers, + HeaderMetadata.Specials + }; + + } + + } + + /// Gets report option. + /// The header. + /// The sort field. + /// The report option. + private ReportOptions GetReportOption(HeaderMetadata header, string sortField = "") + { + ReportHeader reportHeader = new ReportHeader + { + HeaderFieldType = ReportFieldType.String, + SortField = sortField, + Type = "", + ItemViewType = ItemViewType.None + }; + + Func column = null; + Func itemId = null; + HeaderMetadata internalHeader = header; + + switch (header) + { + case HeaderMetadata.StatusImage: + reportHeader.ItemViewType = ItemViewType.StatusImage; + internalHeader = HeaderMetadata.Status; + reportHeader.CanGroup = false; + break; + + case HeaderMetadata.Name: + column = (i, r) => i.Name; + reportHeader.ItemViewType = ItemViewType.Detail; + reportHeader.SortField = "SortName"; + break; + + case HeaderMetadata.DateAdded: + column = (i, r) => i.DateCreated; + reportHeader.SortField = "DateCreated,SortName"; + reportHeader.HeaderFieldType = ReportFieldType.DateTime; + reportHeader.Type = ""; + break; + + case HeaderMetadata.PremiereDate: + case HeaderMetadata.ReleaseDate: + column = (i, r) => i.PremiereDate; + reportHeader.HeaderFieldType = ReportFieldType.DateTime; + reportHeader.SortField = "ProductionYear,PremiereDate,SortName"; + break; + + case HeaderMetadata.Runtime: + column = (i, r) => this.GetRuntimeDateTime(i.RunTimeTicks); + reportHeader.HeaderFieldType = ReportFieldType.Minutes; + reportHeader.SortField = "Runtime,SortName"; + break; + + case HeaderMetadata.PlayCount: + reportHeader.HeaderFieldType = ReportFieldType.Int; + break; + + case HeaderMetadata.Season: + column = (i, r) => this.GetEpisode(i); + reportHeader.ItemViewType = ItemViewType.Detail; + reportHeader.SortField = "SortName"; + break; + + case HeaderMetadata.SeasonNumber: + column = (i, r) => this.GetObject(i, (x) => x.IndexNumber == null ? "" : x.IndexNumber.ToString()); + reportHeader.SortField = "IndexNumber"; + reportHeader.HeaderFieldType = ReportFieldType.Int; + break; + + case HeaderMetadata.Series: + column = (i, r) => this.GetObject(i, (x) => x.SeriesName); + reportHeader.ItemViewType = ItemViewType.Detail; + reportHeader.SortField = "SeriesSortName,SortName"; + break; + + case HeaderMetadata.EpisodeSeries: + column = (i, r) => this.GetObject(i, (x) => x.SeriesName); + reportHeader.ItemViewType = ItemViewType.Detail; + itemId = (i) => + { + Series series = this.GetObject(i, (x) => x.Series); + if (series == null) + return string.Empty; + return series.Id; + }; + reportHeader.SortField = "SeriesSortName,SortName"; + internalHeader = HeaderMetadata.Series; + break; + + case HeaderMetadata.EpisodeSeason: + column = (i, r) => this.GetObject(i, (x) => x.SeriesName); + reportHeader.ItemViewType = ItemViewType.Detail; + itemId = (i) => + { + Season season = this.GetObject(i, (x) => x.Season); + if (season == null) + return string.Empty; + return season.Id; + }; + reportHeader.SortField = "SortName"; + internalHeader = HeaderMetadata.Season; + break; + + case HeaderMetadata.Network: + column = (i, r) => this.GetListAsString(i.Studios); + itemId = (i) => this.GetStudioID(i.Studios.FirstOrDefault()); + reportHeader.ItemViewType = ItemViewType.ItemByNameDetails; + reportHeader.SortField = "Studio,SortName"; + break; + + case HeaderMetadata.Year: + column = (i, r) => this.GetSeriesProductionYear(i); + reportHeader.SortField = "ProductionYear,PremiereDate,SortName"; + break; + + case HeaderMetadata.ParentalRating: + column = (i, r) => i.OfficialRating; + reportHeader.SortField = "OfficialRating,SortName"; + break; + + case HeaderMetadata.CommunityRating: + column = (i, r) => i.CommunityRating; + reportHeader.SortField = "CommunityRating,SortName"; + break; + + case HeaderMetadata.Trailers: + column = (i, r) => this.GetBoolString(r.HasLocalTrailer); + reportHeader.ItemViewType = ItemViewType.TrailersImage; + break; + + case HeaderMetadata.Specials: + column = (i, r) => this.GetBoolString(r.HasSpecials); + reportHeader.ItemViewType = ItemViewType.SpecialsImage; + break; + + case HeaderMetadata.GameSystem: + column = (i, r) => this.GetObject(i, (x) => x.GameSystem); + reportHeader.SortField = "GameSystem,SortName"; + break; + + case HeaderMetadata.Players: + column = (i, r) => this.GetObject(i, (x) => x.PlayersSupported); + reportHeader.SortField = "Players,GameSystem,SortName"; + break; + + case HeaderMetadata.AlbumArtist: + column = (i, r) => this.GetObject(i, (x) => x.AlbumArtist); + itemId = (i) => this.GetPersonID(this.GetObject(i, (x) => x.AlbumArtist)); + reportHeader.ItemViewType = ItemViewType.Detail; + reportHeader.SortField = "AlbumArtist,Album,SortName"; + + break; + case HeaderMetadata.MusicArtist: + column = (i, r) => this.GetObject(i, (x) => x.GetLookupInfo().Name); + reportHeader.ItemViewType = ItemViewType.Detail; + reportHeader.SortField = "AlbumArtist,Album,SortName"; + internalHeader = HeaderMetadata.AlbumArtist; + break; + case HeaderMetadata.AudioAlbumArtist: + column = (i, r) => this.GetListAsString(this.GetObject>(i, (x) => x.AlbumArtists)); + reportHeader.SortField = "AlbumArtist,Album,SortName"; + internalHeader = HeaderMetadata.AlbumArtist; + break; + + case HeaderMetadata.AudioAlbum: + column = (i, r) => this.GetObject(i, (x) => x.Album); + reportHeader.SortField = "Album,SortName"; + internalHeader = HeaderMetadata.Album; + break; + + case HeaderMetadata.Countries: + column = (i, r) => this.GetListAsString(this.GetObject>(i, (x) => x.ProductionLocations)); + break; + + case HeaderMetadata.Disc: + column = (i, r) => i.ParentIndexNumber; + break; + + case HeaderMetadata.Track: + column = (i, r) => i.IndexNumber; + break; + + case HeaderMetadata.Tracks: + column = (i, r) => this.GetObject>(i, (x) => x.Tracks.ToList(), new List