diff options
Diffstat (limited to 'MediaBrowser.Api')
40 files changed, 3820 insertions, 282 deletions
diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 2cd900754..db3dbf048 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -228,7 +228,7 @@ namespace MediaBrowser.Api { lock (_activeTranscodingJobs) { - var job = _activeTranscodingJobs.First(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase)); + var job = _activeTranscodingJobs.First(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase)); _activeTranscodingJobs.Remove(job); } @@ -254,15 +254,7 @@ namespace MediaBrowser.Api { lock (_activeTranscodingJobs) { - return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase)); - } - } - - public TranscodingJob GetTranscodingJob(string id) - { - lock (_activeTranscodingJobs) - { - return _activeTranscodingJobs.FirstOrDefault(j => j.Id.Equals(id, StringComparison.OrdinalIgnoreCase)); + return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase)); } } @@ -275,7 +267,7 @@ namespace MediaBrowser.Api { lock (_activeTranscodingJobs) { - var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase)); + var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase)); if (job == null) { @@ -339,14 +331,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; @@ -459,7 +454,7 @@ namespace MediaBrowser.Api job.DisposeKillTimer(); Logger.Debug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId); - + lock (_activeTranscodingJobs) { _activeTranscodingJobs.Remove(job); @@ -628,6 +623,9 @@ namespace MediaBrowser.Api /// </summary> /// <value>The live stream identifier.</value> public string LiveStreamId { get; set; } + + public bool IsLiveOutput { get; set; } + /// <summary> /// Gets or sets the path. /// </summary> diff --git a/MediaBrowser.Api/Dlna/DlnaServerService.cs b/MediaBrowser.Api/Dlna/DlnaServerService.cs index bdf7d6b07..4f5e2ab25 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 639c1f54b..8c6cc0a18 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 013838091..bab02de35 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 ab205d6eb..0dfd812c3 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -84,10 +84,28 @@ <Compile Include="Playback\MediaInfoService.cs" /> <Compile Include="Playback\TranscodingThrottler.cs" /> <Compile Include="PlaylistService.cs" /> - <Compile Include="Reports\ReportFieldType.cs" /> - <Compile Include="Reports\ReportResult.cs" /> - <Compile Include="Reports\ReportsService.cs" /> + <Compile Include="Reports\Common\HeaderMetadata.cs" /> + <Compile Include="Reports\Common\ItemViewType.cs" /> + <Compile Include="Reports\Common\ReportBuilderBase.cs" /> + <Compile Include="Reports\Common\ReportExportType.cs" /> + <Compile Include="Reports\Common\ReportFieldType.cs" /> + <Compile Include="Reports\Common\ReportHeaderIdType.cs" /> + <Compile Include="Reports\Common\ReportHelper.cs" /> + <Compile Include="Reports\Common\ReportViewType.cs" /> + <Compile Include="Reports\Data\ReportBuilder.cs" /> + <Compile Include="Reports\Data\ReportExport.cs" /> + <Compile Include="Reports\Data\ReportGroup.cs" /> + <Compile Include="Reports\Data\ReportHeader.cs" /> + <Compile Include="Reports\Data\ReportItem.cs" /> + <Compile Include="Reports\Data\ReportOptions.cs" /> + <Compile Include="Reports\Data\ReportResult.cs" /> + <Compile Include="Reports\Data\ReportRow.cs" /> <Compile Include="Reports\ReportRequests.cs" /> + <Compile Include="Reports\ReportsService.cs" /> + <Compile Include="Reports\Stat\ReportStatBuilder.cs" /> + <Compile Include="Reports\Stat\ReportStatGroup.cs" /> + <Compile Include="Reports\Stat\ReportStatItem.cs" /> + <Compile Include="Reports\Stat\ReportStatResult.cs" /> <Compile Include="StartupWizardService.cs" /> <Compile Include="Subtitles\SubtitleService.cs" /> <Compile Include="Movies\CollectionService.cs" /> diff --git a/MediaBrowser.Api/Music/InstantMixService.cs b/MediaBrowser.Api/Music/InstantMixService.cs index 78c6a8bf4..46034dc61 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 1d792fbc1..5ef8b0987 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; } } /// <summary> @@ -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 5e06ab1d0..31679aad3 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -157,11 +157,11 @@ namespace MediaBrowser.Api.Playback /// <value>The fast seek command line parameter.</value> 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 /// <returns>System.Nullable{System.Int32}.</returns> 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 b10c02e17..b2ffeca3d 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 1f6bc242d..6ca5c57f3 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 /// </summary> [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 { } - /// <summary> - /// Class GetHlsVideoSegment - /// </summary> [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; } + + /// <summary> + /// Gets or sets the segment id. + /// </summary> + /// <value>The segment id.</value> + 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<object> Get(GetMasterHlsVideoStream request) + public Task<object> Get(GetMasterHlsVideoPlaylist request) + { + return GetMasterPlaylistInternal(request, "GET"); + } + + public Task<object> Head(GetMasterHlsVideoPlaylist request) + { + return GetMasterPlaylistInternal(request, "HEAD"); + } + + public Task<object> Get(GetMasterHlsAudioPlaylist request) { - return GetAsync(request, "GET"); + return GetMasterPlaylistInternal(request, "GET"); } - public Task<object> Head(GetMasterHlsVideoStream request) + public Task<object> Head(GetMasterHlsAudioPlaylist request) { - return GetAsync(request, "HEAD"); + return GetMasterPlaylistInternal(request, "HEAD"); } - public Task<object> Get(GetMainHlsVideoStream request) + public Task<object> Get(GetVariantHlsVideoPlaylist request) { - return GetPlaylistAsync(request, "main"); + return GetVariantPlaylistInternal(request, true, "main"); } - public Task<object> Get(GetDynamicHlsVideoSegment request) + public Task<object> Get(GetVariantHlsAudioPlaylist request) + { + return GetVariantPlaylistInternal(request, false, "main"); + } + + public Task<object> Get(GetHlsVideoSegment request) + { + return GetDynamicSegment(request, request.SegmentId); + } + + public Task<object> Get(GetHlsAudioSegment request) { return GetDynamicSegment(request, request.SegmentId); } - private async Task<object> GetDynamicSegment(VideoStreamRequest request, string segmentId) + private async Task<object> 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); @@ -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,22 +244,84 @@ 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<string, double> SegmentLengths = new ConcurrentDictionary<string, double>(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; } @@ -292,7 +411,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 +420,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<object> GetSegmentResult(string playlistPath, @@ -325,21 +444,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 +473,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; - - if (newLength == length) - { - eofCount++; - } - else - { - eofCount = 0; - } + //// Wait for the file to stop being written to, then stream it + //var length = new FileInfo(segmentPath).Length; + //var 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 +525,7 @@ namespace MediaBrowser.Api.Playback.Hls }); } - private async Task<object> GetAsync(GetMasterHlsVideoStream request, string method) + private async Task<object> GetMasterPlaylistInternal(StreamRequest request, string method) { var state = await GetState(request, CancellationToken.None).ConfigureAwait(false); @@ -437,14 +562,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 +579,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 +649,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 +671,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 +731,7 @@ namespace MediaBrowser.Api.Playback.Hls return variation; } - private async Task<object> GetPlaylistAsync(VideoStreamRequest request, string name) + private async Task<object> GetVariantPlaylistInternal(StreamRequest request, bool isOutputVideo, string name) { var state = await GetState(request, CancellationToken.None).ConfigureAwait(false); @@ -607,7 +739,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 +755,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 +775,28 @@ namespace MediaBrowser.Api.Playback.Hls protected override string GetAudioArguments(StreamState state) { + if (!state.IsOutputVideo) + { + var audioTranscodeParams = new List<string>(); + 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 +827,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; @@ -682,30 +842,44 @@ namespace MediaBrowser.Api.Playback.Hls } // See if we can save come cpu cycles by avoiding encoding - if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) { - return state.VideoStream != null && IsH264(state.VideoStream) ? - args + " -bsf:v h264_mp4toannexb" : - args; + if (state.VideoStream != null && IsH264(state.VideoStream)) + { + args += " -bsf:v h264_mp4toannexb"; + } + + args += " -flags -global_header -sc_threshold 0"; } + 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); + } + + args += " -flags +loop-global_header -sc_threshold 0"; } - // This is for internal graphical subs - if (hasGraphicalSubs) + if (!EnableSplitTranscoding(state)) { - args += GetGraphicalSubtitleParam(state, codec); + args += " -copyts"; } return args; @@ -715,43 +889,102 @@ 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; + var timestampOffsetParam = 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); - 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(); + if (endTime < state.RunTimeTicks.Value) + { + //toTimeParam = " -to " + MediaEncoder.GetTimeParameter(endTime); + toTimeParam = " -t " + MediaEncoder.GetTimeParameter(TimeSpan.FromSeconds(durationSeconds).Ticks); + } + + if (state.IsOutputVideo && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) && (state.Request.StartTimeTicks ?? 0) > 0) + { + timestampOffsetParam = " -output_ts_offset " + MediaEncoder.GetTimeParameter(state.Request.StartTimeTicks ?? 0).ToString(CultureInfo.InvariantCulture); + } } - 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}\"", + 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} {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; + } + + if (string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return state.RunTimeTicks.HasValue && state.IsOutputVideo; + } + + protected override bool EnableStreamCopy + { + get + { + return false; + } + } + /// <summary> /// Gets the segment file extension. /// </summary> @@ -759,7 +992,12 @@ namespace MediaBrowser.Api.Playback.Hls /// <returns>System.String.</returns> 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 5d8c67abe..b44d7f660 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 + /// <summary> /// Gets or sets the id. /// </summary> @@ -30,11 +32,30 @@ namespace MediaBrowser.Api.Playback.Hls } /// <summary> + /// Class GetHlsVideoStream + /// </summary> + [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; } + } + + /// <summary> /// Class GetHlsVideoSegment /// </summary> [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 /// </summary> [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; } /// <summary> @@ -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 /// </summary> /// <param name="request">The request.</param> /// <returns>System.Object.</returns> - 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 /// </summary> /// <param name="request">The request.</param> /// <returns>System.Object.</returns> - 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 626df59f2..f21be190f 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 { - /// <summary> - /// Class GetHlsVideoStream - /// </summary> - [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 /// </summary> /// <param name="request">The request.</param> /// <returns>System.Object.</returns> - 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 27482c50c..283f9671f 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 { /// <summary> - /// Class GetAudioStream + /// Class GetVideoStream /// </summary> [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 2d1e896db..02b7720a4 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<string> 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 ece455009..fec3dda86 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 4bd78f1f5..4af9bfe58 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<List<PluginInfo>> { + public bool? IsAppStoreEnabled { get; set; } } /// <summary> @@ -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); } /// <summary> @@ -181,6 +190,7 @@ namespace MediaBrowser.Api public async Task<object> 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<PluginInfo>(); + } } return ToOptimizedSerializedResultUsingCache(result); diff --git a/MediaBrowser.Api/Reports/Common/HeaderMetadata.cs b/MediaBrowser.Api/Reports/Common/HeaderMetadata.cs new file mode 100644 index 000000000..3cb8f722d --- /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 000000000..3e09a290d --- /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 000000000..af6dc997c --- /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 +{ + /// <summary> A report builder base. </summary> + public class ReportBuilderBase + { + /// <summary> + /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportBuilderBase class. </summary> + /// <param name="libraryManager"> Manager for library. </param> + public ReportBuilderBase(ILibraryManager libraryManager) + { + _libraryManager = libraryManager; + } + + /// <summary> Manager for library. </summary> + protected readonly ILibraryManager _libraryManager; + + /// <summary> Gets audio stream. </summary> + /// <param name="item"> The item. </param> + /// <returns> The audio stream. </returns> + 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; + } + + /// <summary> Gets an episode. </summary> + /// <param name="item"> The item. </param> + /// <returns> The episode. </returns> + protected string GetEpisode(BaseItem item) + { + + if (item.GetClientTypeName() == ChannelMediaContentType.Episode.ToString() && item.ParentIndexNumber != null) + return "Season " + item.ParentIndexNumber; + else + return item.Name; + } + + /// <summary> Gets a genre. </summary> + /// <param name="name"> The name. </param> + /// <returns> The genre. </returns> + protected Genre GetGenre(string name) + { + if (string.IsNullOrEmpty(name)) + return null; + return _libraryManager.GetGenre(name); + } + + /// <summary> Gets genre identifier. </summary> + /// <param name="name"> The name. </param> + /// <returns> The genre identifier. </returns> + protected string GetGenreID(string name) + { + if (string.IsNullOrEmpty(name)) + return string.Empty; + return string.Format("{0:N}", + GetGenre(name).Id); + } + + /// <summary> Gets list as string. </summary> + /// <param name="items"> The items. </param> + /// <returns> The list as string. </returns> + protected string GetListAsString(List<string> items) + { + return String.Join("; ", items); + } + + /// <summary> Gets media source information. </summary> + /// <param name="item"> The item. </param> + /// <returns> The media source information. </returns> + 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; + } + + /// <summary> Gets an object. </summary> + /// <typeparam name="T"> Generic type parameter. </typeparam> + /// <typeparam name="R"> Type of the r. </typeparam> + /// <param name="item"> The item. </param> + /// <param name="function"> The function. </param> + /// <param name="defaultValue"> The default value. </param> + /// <returns> The object. </returns> + protected R GetObject<T, R>(BaseItem item, Func<T, R> function, R defaultValue = default(R)) where T : class + { + var value = item as T; + if (value != null && function != null) + return function(value); + else + return defaultValue; + } + + /// <summary> Gets a person. </summary> + /// <param name="name"> The name. </param> + /// <returns> The person. </returns> + protected Person GetPerson(string name) + { + if (string.IsNullOrEmpty(name)) + return null; + return _libraryManager.GetPerson(name); + } + + /// <summary> Gets person identifier. </summary> + /// <param name="name"> The name. </param> + /// <returns> The person identifier. </returns> + protected string GetPersonID(string name) + { + if (string.IsNullOrEmpty(name)) + return string.Empty; + return string.Format("{0:N}", + GetPerson(name).Id); + } + + /// <summary> Gets runtime date time. </summary> + /// <param name="runtime"> The runtime. </param> + /// <returns> The runtime date time. </returns> + protected double? GetRuntimeDateTime(long? runtime) + { + if (runtime.HasValue) + return Math.Ceiling(new TimeSpan(runtime.Value).TotalMinutes); + return null; + } + + /// <summary> Gets series production year. </summary> + /// <param name="item"> The item. </param> + /// <returns> The series production year. </returns> + 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; + } + + /// <summary> Gets a stream. </summary> + /// <param name="item"> The item. </param> + /// <param name="streamType"> Type of the stream. </param> + /// <returns> The stream. </returns> + protected MediaStream GetStream(BaseItem item, MediaStreamType streamType) + { + var itemInfo = GetMediaSourceInfo(item); + if (itemInfo != null) + return itemInfo.MediaStreams.FirstOrDefault(n => n.Type == streamType); + + return null; + } + + /// <summary> Gets a studio. </summary> + /// <param name="name"> The name. </param> + /// <returns> The studio. </returns> + protected Studio GetStudio(string name) + { + if (string.IsNullOrEmpty(name)) + return null; + return _libraryManager.GetStudio(name); + } + + /// <summary> Gets studio identifier. </summary> + /// <param name="name"> The name. </param> + /// <returns> The studio identifier. </returns> + protected string GetStudioID(string name) + { + if (string.IsNullOrEmpty(name)) + return string.Empty; + return string.Format("{0:N}", + GetStudio(name).Id); + } + + /// <summary> Gets video resolution. </summary> + /// <param name="item"> The item. </param> + /// <returns> The video resolution. </returns> + 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; + } + + /// <summary> Gets video stream. </summary> + /// <param name="item"> The item. </param> + /// <returns> The video stream. </returns> + 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 000000000..05f27f72e --- /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 000000000..58523657a --- /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 000000000..58c118151 --- /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 000000000..a557248c6 --- /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 + { + /// <summary> Gets java script localized string. </summary> + /// <param name="phrase"> The phrase. </param> + /// <returns> The java script localized string. </returns> + 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; + } + + /// <summary> Gets server localized string. </summary> + /// <param name="phrase"> The phrase. </param> + /// <returns> The server localized string. </returns> + public static string GetServerLocalizedString(string phrase) + { + return BaseItem.LocalizationManager.GetLocalizedString(phrase, BaseItem.ConfigurationManager.Configuration.UICulture); + } + + /// <summary> Gets row type. </summary> + /// <param name="rowType"> The type. </param> + /// <returns> The row type. </returns> + public static ReportViewType GetRowType(string rowType) + { + if (string.IsNullOrEmpty(rowType)) + return ReportViewType.BaseItem; + + ReportViewType rType; + + if (!Enum.TryParse<ReportViewType>(rowType, out rType)) + return ReportViewType.BaseItem; + + return rType; + } + + /// <summary> Gets header metadata type. </summary> + /// <param name="header"> The header. </param> + /// <returns> The header metadata type. </returns> + public static HeaderMetadata GetHeaderMetadataType(string header) + { + if (string.IsNullOrEmpty(header)) + return HeaderMetadata.None; + + HeaderMetadata rType; + + if (!Enum.TryParse<HeaderMetadata>(header, out rType)) + return HeaderMetadata.None; + + return rType; + } + + /// <summary> Convert field to string. </summary> + /// <typeparam name="T"> Generic type parameter. </typeparam> + /// <param name="value"> The value. </param> + /// <param name="fieldType"> Type of the field. </param> + /// <returns> The field converted to string. </returns> + public static string ConvertToString<T>(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 000000000..efdfcb0e7 --- /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 000000000..00ce18317 --- /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 +{ + /// <summary> A report builder. </summary> + /// <seealso cref="T:MediaBrowser.Api.Reports.ReportBuilderBase"/> + public class ReportBuilder : ReportBuilderBase + { + + /// <summary> + /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportBuilder class. </summary> + /// <param name="libraryManager"> Manager for library. </param> + public ReportBuilder(ILibraryManager libraryManager) + : base(libraryManager) + { + } + + private Func<bool, string> GetBoolString = s => s == true ? "x" : ""; + + public ReportResult GetReportResult(BaseItem[] items, ReportViewType reportRowType, BaseReportRequest request) + { + List<HeaderMetadata> 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<ReportHeader> GetReportHeaders(ReportViewType reportRowType, BaseReportRequest request) + { + List<ReportHeader> headersMetadata = this.GetReportHeaders(reportRowType); + if (request != null && !string.IsNullOrEmpty(request.ReportColumns)) + { + List<HeaderMetadata> headersMetadataFiltered = this.GetFilteredReportHeaderMetadata(reportRowType, request); + foreach (ReportHeader reportHeader in headersMetadata) + { + if (!headersMetadataFiltered.Contains(reportHeader.FieldName)) + { + reportHeader.Visible = false; + } + } + + + } + + return headersMetadata; + } + + public List<ReportHeader> GetReportHeaders(ReportViewType reportRowType, List<HeaderMetadata> headersMetadata = null) + { + if (headersMetadata == null) + headersMetadata = this.GetDefaultReportHeaderMetadata(reportRowType); + + List<ReportOptions<BaseItem>> options = new List<ReportOptions<BaseItem>>(); + foreach (HeaderMetadata header in headersMetadata) + { + options.Add(GetReportOption(header)); + } + + + List<ReportHeader> headers = new List<ReportHeader>(); + foreach (ReportOptions<BaseItem> option in options) + { + headers.Add(option.Header); + } + return headers; + } + + private List<ReportRow> GetReportRows(IEnumerable<BaseItem> items, List<HeaderMetadata> headersMetadata) + { + List<ReportOptions<BaseItem>> options = new List<ReportOptions<BaseItem>>(); + foreach (HeaderMetadata header in headersMetadata) + { + options.Add(GetReportOption(header)); + } + + var rows = new List<ReportRow>(); + + foreach (BaseItem item in items) + { + ReportRow rRow = GetRow(item); + foreach (ReportOptions<BaseItem> 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; + } + + /// <summary> Gets a row. </summary> + /// <param name="item"> The item. </param> + /// <returns> The row. </returns> + 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<HeaderMetadata> 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<HeaderMetadata> GetDefaultReportHeaderMetadata(ReportViewType reportRowType) + { + switch (reportRowType) + { + case ReportViewType.Season: + return new List<HeaderMetadata> + { + HeaderMetadata.StatusImage, + HeaderMetadata.Series, + HeaderMetadata.Season, + HeaderMetadata.SeasonNumber, + HeaderMetadata.DateAdded, + HeaderMetadata.Year, + HeaderMetadata.Genres + }; + + case ReportViewType.Series: + return new List<HeaderMetadata> + { + 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> + { + HeaderMetadata.StatusImage, + HeaderMetadata.Name, + HeaderMetadata.AlbumArtist, + HeaderMetadata.DateAdded, + HeaderMetadata.ReleaseDate, + HeaderMetadata.Tracks, + HeaderMetadata.Year, + HeaderMetadata.Genres + }; + + case ReportViewType.MusicArtist: + return new List<HeaderMetadata> + { + HeaderMetadata.StatusImage, + HeaderMetadata.MusicArtist, + HeaderMetadata.Countries, + HeaderMetadata.DateAdded, + HeaderMetadata.Year, + HeaderMetadata.Genres + }; + + case ReportViewType.Game: + return new List<HeaderMetadata> + { + 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> + { + 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> + { + HeaderMetadata.StatusImage, + HeaderMetadata.Name, + HeaderMetadata.DateAdded, + HeaderMetadata.ReleaseDate, + HeaderMetadata.Year, + HeaderMetadata.Genres, + HeaderMetadata.ParentalRating, + HeaderMetadata.CommunityRating + }; + + case ReportViewType.BoxSet: + return new List<HeaderMetadata> + { + 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> + { + 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> + { + 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> + { + 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 + }; + + } + + } + + /// <summary> Gets report option. </summary> + /// <param name="header"> The header. </param> + /// <param name="sortField"> The sort field. </param> + /// <returns> The report option. </returns> + private ReportOptions<BaseItem> GetReportOption(HeaderMetadata header, string sortField = "") + { + ReportHeader reportHeader = new ReportHeader + { + HeaderFieldType = ReportFieldType.String, + SortField = sortField, + Type = "", + ItemViewType = ItemViewType.None + }; + + Func<BaseItem, ReportRow, object> column = null; + Func<BaseItem, object> 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<Season, string>(i, (x) => x.IndexNumber == null ? "" : x.IndexNumber.ToString()); + reportHeader.SortField = "IndexNumber"; + reportHeader.HeaderFieldType = ReportFieldType.Int; + break; + + case HeaderMetadata.Series: + column = (i, r) => this.GetObject<IHasSeries, string>(i, (x) => x.SeriesName); + reportHeader.ItemViewType = ItemViewType.Detail; + reportHeader.SortField = "SeriesSortName,SortName"; + break; + + case HeaderMetadata.EpisodeSeries: + column = (i, r) => this.GetObject<IHasSeries, string>(i, (x) => x.SeriesName); + reportHeader.ItemViewType = ItemViewType.Detail; + itemId = (i) => + { + Series series = this.GetObject<Episode, Series>(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<IHasSeries, string>(i, (x) => x.SeriesName); + reportHeader.ItemViewType = ItemViewType.Detail; + itemId = (i) => + { + Season season = this.GetObject<Episode, Season>(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<Game, string>(i, (x) => x.GameSystem); + reportHeader.SortField = "GameSystem,SortName"; + break; + + case HeaderMetadata.Players: + column = (i, r) => this.GetObject<Game, int?>(i, (x) => x.PlayersSupported); + reportHeader.SortField = "Players,GameSystem,SortName"; + break; + + case HeaderMetadata.AlbumArtist: + column = (i, r) => this.GetObject<MusicAlbum, string>(i, (x) => x.AlbumArtist); + itemId = (i) => this.GetPersonID(this.GetObject<MusicAlbum, string>(i, (x) => x.AlbumArtist)); + reportHeader.ItemViewType = ItemViewType.Detail; + reportHeader.SortField = "AlbumArtist,Album,SortName"; + + break; + case HeaderMetadata.MusicArtist: + column = (i, r) => this.GetObject<MusicArtist, string>(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<Audio, List<string>>(i, (x) => x.AlbumArtists)); + reportHeader.SortField = "AlbumArtist,Album,SortName"; + internalHeader = HeaderMetadata.AlbumArtist; + break; + + case HeaderMetadata.AudioAlbum: + column = (i, r) => this.GetObject<Audio, string>(i, (x) => x.Album); + reportHeader.SortField = "Album,SortName"; + internalHeader = HeaderMetadata.Album; + break; + + case HeaderMetadata.Countries: + column = (i, r) => this.GetListAsString(this.GetObject<IHasProductionLocations, List<string>>(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<MusicAlbum, List<Audio>>(i, (x) => x.Tracks.ToList(), new List<Audio>()).Count(); + break; + + case HeaderMetadata.Audio: + column = (i, r) => this.GetAudioStream(i); + break; + + case HeaderMetadata.EmbeddedImage: + break; + + case HeaderMetadata.Video: + column = (i, r) => this.GetVideoStream(i); + break; + + case HeaderMetadata.Resolution: + column = (i, r) => this.GetVideoResolution(i); + break; + + case HeaderMetadata.Subtitles: + column = (i, r) => this.GetBoolString(r.HasSubtitles); + reportHeader.ItemViewType = ItemViewType.SubtitleImage; + break; + + case HeaderMetadata.Genres: + column = (i, r) => this.GetListAsString(i.Genres); + break; + + } + + string headerName = ""; + if (internalHeader != HeaderMetadata.None) + { + string localHeader = "Header" + internalHeader.ToString(); + headerName = internalHeader != HeaderMetadata.None ? ReportHelper.GetJavaScriptLocalizedString(localHeader) : ""; + if (string.Compare(localHeader, headerName, StringComparison.CurrentCultureIgnoreCase) == 0) + headerName = ReportHelper.GetServerLocalizedString(localHeader); + } + + reportHeader.Name = headerName; + reportHeader.FieldName = header; + ReportOptions<BaseItem> option = new ReportOptions<BaseItem>() + { + Header = reportHeader, + Column = column, + ItemID = itemId + }; + return option; + } + } +} diff --git a/MediaBrowser.Api/Reports/Data/ReportExport.cs b/MediaBrowser.Api/Reports/Data/ReportExport.cs new file mode 100644 index 000000000..f313cf252 --- /dev/null +++ b/MediaBrowser.Api/Reports/Data/ReportExport.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.Reports +{ + /// <summary> A report export. </summary> + public class ReportExport + { + /// <summary> Export to CSV. </summary> + /// <param name="reportResult"> The report result. </param> + /// <returns> A string. </returns> + public string ExportToCsv(ReportResult reportResult) + { + StringBuilder returnValue = new StringBuilder(); + + returnValue.AppendLine(string.Join(";", reportResult.Headers.Select(s => s.Name.Replace(',', ' ')).ToArray())); + + if (reportResult.IsGrouped) + foreach (ReportGroup group in reportResult.Groups) + { + foreach (ReportRow row in reportResult.Rows) + { + returnValue.AppendLine(string.Join(";", row.Columns.Select(s => s.Name.Replace(',', ' ')).ToArray())); + } + } + else + foreach (ReportRow row in reportResult.Rows) + { + returnValue.AppendLine(string.Join(";", row.Columns.Select(s => s.Name.Replace(',', ' ')).ToArray())); + } + + return returnValue.ToString(); + } + + + /// <summary> Export to excel. </summary> + /// <param name="reportResult"> The report result. </param> + /// <returns> A string. </returns> + public string ExportToExcel(ReportResult reportResult) + { + + string style = @"<style type='text/css'> + BODY { + font-family: Arial; + font-size: 12px; + } + + TABLE { + font-family: Arial; + font-size: 12px; + } + + A { + font-family: Arial; + color: #144A86; + font-size: 12px; + cursor: pointer; + text-decoration: none; + font-weight: bold; + } + DIV { + font-family: Arial; + font-size: 12px; + margin-bottom: 0px; + } + P, LI, DIV { + font-size: 12px; + margin-bottom: 0px; + } + + P, UL { + font-size: 12px; + margin-bottom: 6px; + margin-top: 0px; + } + + H1 { + font-size: 18pt; + } + + H2 { + font-weight: bold; + font-size: 14pt; + COLOR: #C0C0C0; + } + + H3 { + font-weight: normal; + font-size: 14pt; + text-indent: +1em; + } + + H4 { + font-size: 10pt; + font-weight: normal; + } + + H5 { + font-size: 10pt; + font-weight: normal; + background: #A9A9A9; + COLOR: white; + display: inline; + } + + H6 { + padding: 2 1 2 5; + font-size: 11px; + font-weight: bold; + text-decoration: none; + margin-bottom: 1px; + } + + UL { + line-height: 1.5em; + list-style-type: disc; + } + + OL { + line-height: 1.5em; + } + + LI { + line-height: 1.5em; + } + + A IMG { + border: 0; + } + + table.gridtable { + color: #333333; + border-width: 0.1pt; + border-color: #666666; + border-collapse: collapse; + } + + table.gridtable th { + border-width: 0.1pt; + padding: 8px; + border-style: solid; + border-color: #666666; + background-color: #dedede; + } + table.gridtable tr { + background-color: #ffffff; + } + table.gridtable td { + border-width: 0.1pt; + padding: 8px; + border-style: solid; + border-color: #666666; + background-color: #ffffff; + } + </style>"; + + string Html = @"<!DOCTYPE html> + <html xmlns='http://www.w3.org/1999/xhtml'> + <head> + <meta http-equiv='X-UA-Compatible' content='IE=8, IE=9, IE=10' /> + <meta charset='utf-8'> + <title>Emby Reports Export</title>"; + Html += "\n" + style + "\n"; + Html += "</head>\n"; + Html += "<body>\n"; + + StringBuilder returnValue = new StringBuilder(); + returnValue.AppendLine("<table class='gridtable'>"); + returnValue.AppendLine("<tr>"); + returnValue.AppendLine(string.Join("", reportResult.Headers.Select(s => string.Format("<th>{0}</th>", s.Name)).ToArray())); + returnValue.AppendLine("</tr>"); + if (reportResult.IsGrouped) + foreach (ReportGroup group in reportResult.Groups) + { + returnValue.AppendLine("<tr>"); + returnValue.AppendLine("<th scope='rowgroup' colspan='" + reportResult.Headers.Count + "'>" + (string.IsNullOrEmpty(group.Name) ? " " : group.Name) + "</th>"); + returnValue.AppendLine("</tr>"); + foreach (ReportRow row in group.Rows) + { + ExportToExcelRow(reportResult, returnValue, row); + } + returnValue.AppendLine("<tr>"); + returnValue.AppendLine("<th style='background-color: #ffffff;' scope='rowgroup' colspan='" + reportResult.Headers.Count + "'>" + " " + "</th>"); + returnValue.AppendLine("</tr>"); + } + + else + foreach (ReportRow row in reportResult.Rows) + { + ExportToExcelRow(reportResult, returnValue, row); + } + returnValue.AppendLine("</table>"); + + Html += returnValue.ToString(); + Html += "</body>"; + Html += "</html>"; + return Html; + } + private static void ExportToExcelRow(ReportResult reportResult, + StringBuilder returnValue, + ReportRow row) + { + returnValue.AppendLine("<tr>"); + returnValue.AppendLine(string.Join("", row.Columns.Select(s => string.Format("<td>{0}</td>", s.Name)).ToArray())); + returnValue.AppendLine("</tr>"); + } + } + +} diff --git a/MediaBrowser.Api/Reports/Data/ReportGroup.cs b/MediaBrowser.Api/Reports/Data/ReportGroup.cs new file mode 100644 index 000000000..49c76c7ba --- /dev/null +++ b/MediaBrowser.Api/Reports/Data/ReportGroup.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.Reports +{ + + /// <summary> A report group. </summary> + public class ReportGroup + { + /// <summary> + /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportGroup class. </summary> + public ReportGroup() + { + Rows = new List<ReportRow>(); + } + + /// <summary> + /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportGroup class. </summary> + /// <param name="rows"> The rows. </param> + public ReportGroup(List<ReportRow> rows) + { + Rows = rows; + } + + /// <summary> Gets or sets the name. </summary> + /// <value> The name. </value> + public string Name { get; set; } + + /// <summary> Gets or sets the rows. </summary> + /// <value> The rows. </value> + public List<ReportRow> Rows { get; set; } + + /// <summary> Returns a string that represents the current object. </summary> + /// <returns> A string that represents the current object. </returns> + /// <seealso cref="M:System.Object.ToString()"/> + public override string ToString() + { + return Name; + } + } +} diff --git a/MediaBrowser.Api/Reports/Data/ReportHeader.cs b/MediaBrowser.Api/Reports/Data/ReportHeader.cs new file mode 100644 index 000000000..81b85954a --- /dev/null +++ b/MediaBrowser.Api/Reports/Data/ReportHeader.cs @@ -0,0 +1,54 @@ +using MediaBrowser.Controller.Entities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.Reports +{ + /// <summary> A report header. </summary> + public class ReportHeader + { + /// <summary> Initializes a new instance of the ReportHeader class. </summary> + public ReportHeader() + { + ItemViewType = ItemViewType.None; + Visible = true; + CanGroup = true; + } + + /// <summary> Gets or sets the type of the header field. </summary> + /// <value> The type of the header field. </value> + public ReportFieldType HeaderFieldType { get; set; } + + /// <summary> Gets or sets the name of the header. </summary> + /// <value> The name of the header. </value> + public string Name { get; set; } + + /// <summary> Gets or sets the name of the field. </summary> + /// <value> The name of the field. </value> + public HeaderMetadata FieldName { get; set; } + + /// <summary> Gets or sets the sort field. </summary> + /// <value> The sort field. </value> + public string SortField { get; set; } + + /// <summary> Gets or sets the type. </summary> + /// <value> The type. </value> + public string Type { get; set; } + + /// <summary> Gets or sets the type of the item view. </summary> + /// <value> The type of the item view. </value> + public ItemViewType ItemViewType { get; set; } + + /// <summary> Gets or sets a value indicating whether this object is visible. </summary> + /// <value> true if visible, false if not. </value> + public bool Visible { get; set; } + + /// <summary> Gets or sets a value indicating whether we can group. </summary> + /// <value> true if we can group, false if not. </value> + public bool CanGroup { get; set; } + + } +} diff --git a/MediaBrowser.Api/Reports/Data/ReportItem.cs b/MediaBrowser.Api/Reports/Data/ReportItem.cs new file mode 100644 index 000000000..06d0b0c46 --- /dev/null +++ b/MediaBrowser.Api/Reports/Data/ReportItem.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.Reports +{ + /// <summary> A report item. </summary> + public class ReportItem + { + /// <summary> Gets or sets the identifier. </summary> + /// <value> The identifier. </value> + public string Id { get; set; } + + /// <summary> Gets or sets the name. </summary> + /// <value> The name. </value> + public string Name { get; set; } + + public string Image { get; set; } + + /// <summary> Gets or sets the custom tag. </summary> + /// <value> The custom tag. </value> + public string CustomTag { get; set; } + + /// <summary> Returns a string that represents the current object. </summary> + /// <returns> A string that represents the current object. </returns> + /// <seealso cref="M:System.Object.ToString()"/> + public override string ToString() + { + return Name; + } + } +} diff --git a/MediaBrowser.Api/Reports/Data/ReportOptions.cs b/MediaBrowser.Api/Reports/Data/ReportOptions.cs new file mode 100644 index 000000000..aed15d428 --- /dev/null +++ b/MediaBrowser.Api/Reports/Data/ReportOptions.cs @@ -0,0 +1,52 @@ +using MediaBrowser.Controller.Entities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.Reports +{ + /// <summary> A report options. </summary> + internal class ReportOptions<I> + { + /// <summary> Initializes a new instance of the ReportOptions class. </summary> + public ReportOptions() + { + } + + /// <summary> Initializes a new instance of the ReportOptions class. </summary> + /// <param name="header"> . </param> + /// <param name="row"> . </param> + public ReportOptions(ReportHeader header, Func<I, ReportRow, object> column) + { + Header = header; + Column = column; + } + + /// <summary> + /// Initializes a new instance of the ReportOptions class. + /// </summary> + /// <param name="header"></param> + /// <param name="column"></param> + /// <param name="itemID"></param> + public ReportOptions(ReportHeader header, Func<I, ReportRow, object> column, Func<I, object> itemID) + { + Header = header; + Column = column; + ItemID = itemID; + } + + /// <summary> Gets or sets the header. </summary> + /// <value> The header. </value> + public ReportHeader Header { get; set; } + + /// <summary> Gets or sets the column. </summary> + /// <value> The column. </value> + public Func<I, ReportRow, object> Column { get; set; } + + /// <summary> Gets or sets the identifier of the item. </summary> + /// <value> The identifier of the item. </value> + public Func<I, object> ItemID { get; set; } + } +} diff --git a/MediaBrowser.Api/Reports/Data/ReportResult.cs b/MediaBrowser.Api/Reports/Data/ReportResult.cs new file mode 100644 index 000000000..a4bc95aa1 --- /dev/null +++ b/MediaBrowser.Api/Reports/Data/ReportResult.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Api.Reports +{ + + /// <summary> Encapsulates the result of a report. </summary> + public class ReportResult + { + /// <summary> + /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportResult class. </summary> + public ReportResult() + { + Rows = new List<ReportRow>(); + Headers = new List<ReportHeader>(); + Groups = new List<ReportGroup>(); + TotalRecordCount = 0; + IsGrouped = false; + } + + /// <summary> + /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportResult class. </summary> + /// <param name="headers"> The headers. </param> + /// <param name="rows"> The rows. </param> + public ReportResult(List<ReportHeader> headers, List<ReportRow> rows) + { + Rows = rows; + Headers = headers; + TotalRecordCount = 0; + } + + /// <summary> Gets or sets the rows. </summary> + /// <value> The rows. </value> + public List<ReportRow> Rows { get; set; } + + /// <summary> Gets or sets the headers. </summary> + /// <value> The headers. </value> + public List<ReportHeader> Headers { get; set; } + + /// <summary> Gets or sets the groups. </summary> + /// <value> The groups. </value> + public List<ReportGroup> Groups { get; set; } + + + /// <summary> Gets or sets the number of total records. </summary> + /// <value> The total number of record count. </value> + public int TotalRecordCount { get; set; } + + /// <summary> Gets or sets the is grouped. </summary> + /// <value> The is grouped. </value> + public bool IsGrouped { get; set; } + + } +} diff --git a/MediaBrowser.Api/Reports/Data/ReportRow.cs b/MediaBrowser.Api/Reports/Data/ReportRow.cs new file mode 100644 index 000000000..f2165344a --- /dev/null +++ b/MediaBrowser.Api/Reports/Data/ReportRow.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.Reports +{ + public class ReportRow + { + /// <summary> + /// Initializes a new instance of the ReportRow class. + /// </summary> + public ReportRow() + { + Columns = new List<ReportItem>(); + } + + /// <summary> Gets or sets the identifier. </summary> + /// <value> The identifier. </value> + public string Id { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this object has backdrop image. </summary> + /// <value> true if this object has backdrop image, false if not. </value> + public bool HasImageTagsBackdrop { get; set; } + + /// <summary> Gets or sets a value indicating whether this object has image tags. </summary> + /// <value> true if this object has image tags, false if not. </value> + public bool HasImageTagsPrimary { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this object has image tags logo. </summary> + /// <value> true if this object has image tags logo, false if not. </value> + public bool HasImageTagsLogo { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this object has local trailer. </summary> + /// <value> true if this object has local trailer, false if not. </value> + public bool HasLocalTrailer { get; set; } + + /// <summary> Gets or sets a value indicating whether this object has lock data. </summary> + /// <value> true if this object has lock data, false if not. </value> + public bool HasLockData { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this object has embedded image. </summary> + /// <value> true if this object has embedded image, false if not. </value> + public bool HasEmbeddedImage { get; set; } + + /// <summary> Gets or sets a value indicating whether this object has subtitles. </summary> + /// <value> true if this object has subtitles, false if not. </value> + public bool HasSubtitles { get; set; } + + /// <summary> Gets or sets a value indicating whether this object has specials. </summary> + /// <value> true if this object has specials, false if not. </value> + public bool HasSpecials { get; set; } + + /// <summary> Gets or sets a value indicating whether this object is unidentified. </summary> + /// <value> true if this object is unidentified, false if not. </value> + public bool IsUnidentified { get; set; } + + /// <summary> Gets or sets the columns. </summary> + /// <value> The columns. </value> + public List<ReportItem> Columns { get; set; } + + /// <summary> Gets or sets the type. </summary> + /// <value> The type. </value> + public ReportViewType RowType { get; set; } + } +} diff --git a/MediaBrowser.Api/Reports/ReportFieldType.cs b/MediaBrowser.Api/Reports/ReportFieldType.cs deleted file mode 100644 index d35c5cb2d..000000000 --- a/MediaBrowser.Api/Reports/ReportFieldType.cs +++ /dev/null @@ -1,9 +0,0 @@ - -namespace MediaBrowser.Api.Reports -{ - public enum ReportFieldType - { - String, - Boolean - } -} diff --git a/MediaBrowser.Api/Reports/ReportRequests.cs b/MediaBrowser.Api/Reports/ReportRequests.cs index 8dea00381..939b49282 100644 --- a/MediaBrowser.Api/Reports/ReportRequests.cs +++ b/MediaBrowser.Api/Reports/ReportRequests.cs @@ -1,33 +1,287 @@ -using ServiceStack; +using System; +using System.Linq; +using MediaBrowser.Api.UserLibrary; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Entities; +using ServiceStack; +using System.Collections.Generic; namespace MediaBrowser.Api.Reports { - public class BaseReportRequest : IReturn<ReportResult> - { + public class BaseReportRequest : BaseItemsRequest + { /// <summary> - /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Gets or sets the user id. /// </summary> - /// <value>The parent id.</value> - [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string ParentId { get; set; } + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")] + public Guid? UserId { get; set; } /// <summary> - /// Skips over a given number of items within the results. Use for paging. + /// Limit results to items containing a specific person /// </summary> - /// <value>The start index.</value> - [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? StartIndex { get; set; } + /// <value>The person.</value> + [ApiMember(Name = "Person", Description = "Optional. If specified, results will be filtered to include only those containing the specified person.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string Person { get; set; } + + [ApiMember(Name = "PersonIds", Description = "Optional. If specified, results will be filtered to include only those containing the specified person.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string PersonIds { get; set; } /// <summary> - /// The maximum number of items to return + /// If the Person filter is used, this can also be used to restrict to a specific person type /// </summary> - /// <value>The limit.</value> - [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? Limit { get; set; } - } + /// <value>The type of the person.</value> + [ApiMember(Name = "PersonTypes", Description = "Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string PersonTypes { get; set; } + + /// <summary> + /// Limit results to items containing specific studios + /// </summary> + /// <value>The studios.</value> + [ApiMember(Name = "Studios", Description = "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string Studios { get; set; } + + [ApiMember(Name = "StudioIds", Description = "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string StudioIds { get; set; } + + /// <summary> + /// Gets or sets the studios. + /// </summary> + /// <value>The studios.</value> + [ApiMember(Name = "Artists", Description = "Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string Artists { get; set; } + + [ApiMember(Name = "ArtistIds", Description = "Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string ArtistIds { get; set; } + + [ApiMember(Name = "Albums", Description = "Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string Albums { get; set; } + + /// <summary> + /// Gets or sets the item ids. + /// </summary> + /// <value>The item ids.</value> + [ApiMember(Name = "Ids", Description = "Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string Ids { get; set; } + + public bool HasQueryLimit { get; set; } + public string GroupBy { get; set; } + + public string ReportColumns { get; set; } + + /// <summary> + /// Gets or sets the video types. + /// </summary> + /// <value>The video types.</value> + [ApiMember(Name = "VideoTypes", Description = "Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string VideoTypes { get; set; } + + /// <summary> + /// Gets or sets the video formats. + /// </summary> + /// <value>The video formats.</value> + [ApiMember(Name = "Is3D", Description = "Optional filter by items that are 3D, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? Is3D { get; set; } + + /// <summary> + /// Gets or sets the series status. + /// </summary> + /// <value>The series status.</value> + [ApiMember(Name = "SeriesStatus", Description = "Optional filter by Series Status. Allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string SeriesStatus { get; set; } + + [ApiMember(Name = "NameStartsWithOrGreater", Description = "Optional filter by items whose name is sorted equally or greater than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string NameStartsWithOrGreater { get; set; } + + [ApiMember(Name = "NameStartsWith", Description = "Optional filter by items whose name is sorted equally than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string NameStartsWith { get; set; } + + [ApiMember(Name = "NameLessThan", Description = "Optional filter by items whose name is equally or lesser than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string NameLessThan { get; set; } + + [ApiMember(Name = "AlbumArtistStartsWithOrGreater", Description = "Optional filter by items whose album artist is sorted equally or greater than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string AlbumArtistStartsWithOrGreater { get; set; } + + /// <summary> + /// Gets or sets the air days. + /// </summary> + /// <value>The air days.</value> + [ApiMember(Name = "AirDays", Description = "Optional filter by Series Air Days. Allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string AirDays { get; set; } + + /// <summary> + /// Gets or sets the min offical rating. + /// </summary> + /// <value>The min offical rating.</value> + [ApiMember(Name = "MinOfficialRating", Description = "Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string MinOfficialRating { get; set; } + + /// <summary> + /// Gets or sets the max offical rating. + /// </summary> + /// <value>The max offical rating.</value> + [ApiMember(Name = "MaxOfficialRating", Description = "Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string MaxOfficialRating { get; set; } + + [ApiMember(Name = "HasThemeSong", Description = "Optional filter by items with theme songs.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public bool? HasThemeSong { get; set; } + + [ApiMember(Name = "HasThemeVideo", Description = "Optional filter by items with theme videos.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public bool? HasThemeVideo { get; set; } + + [ApiMember(Name = "HasSubtitles", Description = "Optional filter by items with subtitles.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public bool? HasSubtitles { get; set; } + + [ApiMember(Name = "HasSpecialFeature", Description = "Optional filter by items with special features.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public bool? HasSpecialFeature { get; set; } + + [ApiMember(Name = "HasTrailer", Description = "Optional filter by items with trailers.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public bool? HasTrailer { get; set; } - [Route("/Reports/Items", "GET", Summary = "Gets reports based on library items")] - public class GetItemReport : BaseReportRequest - { + [ApiMember(Name = "AdjacentTo", Description = "Optional. Return items that are siblings of a supplied item.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string AdjacentTo { get; set; } + + [ApiMember(Name = "MinIndexNumber", Description = "Optional filter by minimum index number.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? MinIndexNumber { get; set; } + + [ApiMember(Name = "MinPlayers", Description = "Optional filter by minimum number of game players.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? MinPlayers { get; set; } + + [ApiMember(Name = "MaxPlayers", Description = "Optional filter by maximum number of game players.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? MaxPlayers { get; set; } + + [ApiMember(Name = "ParentIndexNumber", Description = "Optional filter by parent index number.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? ParentIndexNumber { get; set; } + + [ApiMember(Name = "HasParentalRating", Description = "Optional filter by items that have or do not have a parental rating", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? HasParentalRating { get; set; } + + [ApiMember(Name = "IsHD", Description = "Optional filter by items that are HD or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? IsHD { get; set; } + + [ApiMember(Name = "LocationTypes", Description = "Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string LocationTypes { get; set; } + + [ApiMember(Name = "ExcludeLocationTypes", Description = "Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string ExcludeLocationTypes { get; set; } + + [ApiMember(Name = "IsMissing", Description = "Optional filter by items that are missing episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? IsMissing { get; set; } + + [ApiMember(Name = "IsUnaired", Description = "Optional filter by items that are unaired episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? IsUnaired { get; set; } + + [ApiMember(Name = "IsVirtualUnaired", Description = "Optional filter by items that are virtual unaired episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? IsVirtualUnaired { get; set; } + + [ApiMember(Name = "MinCommunityRating", Description = "Optional filter by minimum community rating.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public double? MinCommunityRating { get; set; } + + [ApiMember(Name = "MinCriticRating", Description = "Optional filter by minimum critic rating.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public double? MinCriticRating { get; set; } + + [ApiMember(Name = "AiredDuringSeason", Description = "Gets all episodes that aired during a season, including specials.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? AiredDuringSeason { get; set; } + + [ApiMember(Name = "MinPremiereDate", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string MinPremiereDate { get; set; } + + [ApiMember(Name = "MaxPremiereDate", Description = "Optional. The maximum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string MaxPremiereDate { get; set; } + + [ApiMember(Name = "HasOverview", Description = "Optional filter by items that have an overview or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? HasOverview { get; set; } + + [ApiMember(Name = "HasImdbId", Description = "Optional filter by items that have an imdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? HasImdbId { get; set; } + + [ApiMember(Name = "HasTmdbId", Description = "Optional filter by items that have a tmdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? HasTmdbId { get; set; } + + [ApiMember(Name = "HasTvdbId", Description = "Optional filter by items that have a tvdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? HasTvdbId { get; set; } + + [ApiMember(Name = "IsYearMismatched", Description = "Optional filter by items that are potentially misidentified.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? IsYearMismatched { get; set; } + + [ApiMember(Name = "IsInBoxSet", Description = "Optional filter by items that are in boxsets, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? IsInBoxSet { get; set; } + + [ApiMember(Name = "IsLocked", Description = "Optional filter by items that are locked.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public bool? IsLocked { get; set; } + + [ApiMember(Name = "IsUnidentified", Description = "Optional filter by items that are unidentified by internet metadata providers.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public bool? IsUnidentified { get; set; } + + [ApiMember(Name = "IsPlaceHolder", Description = "Optional filter by items that are placeholders", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public bool? IsPlaceHolder { get; set; } + + [ApiMember(Name = "HasOfficialRating", Description = "Optional filter by items that have official ratings", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public bool? HasOfficialRating { get; set; } + + [ApiMember(Name = "CollapseBoxSetItems", Description = "Whether or not to hide items behind their boxsets.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? CollapseBoxSetItems { get; set; } + + public string[] GetStudios() + { + return (Studios ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + } + + public string[] GetStudioIds() + { + return (StudioIds ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + } + + public string[] GetPersonTypes() + { + return (PersonTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + + public string[] GetPersonIds() + { + return (PersonIds ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + + public VideoType[] GetVideoTypes() + { + var val = VideoTypes; + + if (string.IsNullOrEmpty(val)) + { + return new VideoType[] { }; + } + + return val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => (VideoType)Enum.Parse(typeof(VideoType), v, true)).ToArray(); + } } + + [Route("/Reports/Items", "GET", Summary = "Gets reports based on library items")] + public class GetItemReport : BaseReportRequest, IReturn<ReportResult> + { + + } + + [Route("/Reports/Headers", "GET", Summary = "Gets reports headers based on library items")] + public class GetReportHeaders : BaseReportRequest, IReturn<List<ReportHeader>> + { + } + + [Route("/Reports/Statistics", "GET", Summary = "Gets reports statistics based on library items")] + public class GetReportStatistics : BaseReportRequest, IReturn<ReportStatResult> + { + public int? TopItems { get; set; } + + } + + [Route("/Reports/Items/Download", "GET", Summary = "Downloads report")] + public class GetReportDownload : BaseReportRequest + { + public GetReportDownload() + { + ExportType = ReportExportType.CSV; + } + + public ReportExportType ExportType { get; set; } + } + } diff --git a/MediaBrowser.Api/Reports/ReportResult.cs b/MediaBrowser.Api/Reports/ReportResult.cs deleted file mode 100644 index c033ae8fb..000000000 --- a/MediaBrowser.Api/Reports/ReportResult.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; - -namespace MediaBrowser.Api.Reports -{ - public class ReportResult - { - public List<List<string>> Rows { get; set; } - public List<ReportFieldType> Columns { get; set; } - - public ReportResult() - { - Rows = new List<List<string>>(); - Columns = new List<ReportFieldType>(); - } - } -} diff --git a/MediaBrowser.Api/Reports/ReportsService.cs b/MediaBrowser.Api/Reports/ReportsService.cs index 45bc4a889..b13e5628c 100644 --- a/MediaBrowser.Api/Reports/ReportsService.cs +++ b/MediaBrowser.Api/Reports/ReportsService.cs @@ -1,64 +1,1163 @@ -using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Querying; +using System.Collections.Generic; using System.Threading.Tasks; +using System.Globalization; +using System.Linq; +using MediaBrowser.Model.Dto; +using MediaBrowser.Controller.Localization; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Api.UserLibrary; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.Entities.TV; +using System; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Model.Entities; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Activity; +using MediaBrowser.Controller.Activity; +using System.IO; +using System.Text; namespace MediaBrowser.Api.Reports { - public class ReportsService : BaseApiService - { - private readonly ILibraryManager _libraryManager; + /// <summary> The reports service. </summary> + /// <seealso cref="T:MediaBrowser.Api.BaseApiService"/> + public class ReportsService : BaseApiService + { - public ReportsService(ILibraryManager libraryManager) - { - _libraryManager = libraryManager; - } - public async Task<object> Get(GetItemReport request) - { - var queryResult = await GetQueryResult(request).ConfigureAwait(false); + /// <summary> Manager for user. </summary> + private readonly IUserManager _userManager; - var reportResult = GetReportResult(queryResult); + /// <summary> Manager for library. </summary> + private readonly ILibraryManager _libraryManager; + /// <summary> The localization. </summary> + private readonly ILocalizationManager _localization; - return ToOptimizedResult(reportResult); - } + /// <summary> + /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportsService class. </summary> + /// <param name="userManager"> Manager for user. </param> + /// <param name="libraryManager"> Manager for library. </param> + /// <param name="localization"> The localization. </param> + public ReportsService(IUserManager userManager, ILibraryManager libraryManager, ILocalizationManager localization) + { + _userManager = userManager; + _libraryManager = libraryManager; + _localization = localization; + } - private ReportResult GetReportResult(QueryResult<BaseItem> queryResult) - { - var reportResult = new ReportResult(); + /// <summary> Gets the given request. </summary> + /// <param name="request"> The request. </param> + /// <returns> A Task<object> </returns> + public async Task<object> Get(GetReportHeaders request) + { + if (string.IsNullOrEmpty(request.IncludeItemTypes)) + return null; - // Fill rows and columns + ReportViewType reportRowType = ReportHelper.GetRowType(request.IncludeItemTypes); + ReportBuilder reportBuilder = new ReportBuilder(_libraryManager); + var reportResult = reportBuilder.GetReportHeaders(reportRowType, request); - return reportResult; - } + return ToOptimizedResult(reportResult); - private Task<QueryResult<BaseItem>> GetQueryResult(BaseReportRequest request) - { - // Placeholder in case needed later - User user = null; + } - var parentItem = string.IsNullOrEmpty(request.ParentId) ? - (user == null ? _libraryManager.RootFolder : user.RootFolder) : - _libraryManager.GetItemById(request.ParentId); + /// <summary> Gets the given request. </summary> + /// <param name="request"> The request. </param> + /// <returns> A Task<object> </returns> + public async Task<object> Get(GetItemReport request) + { + if (string.IsNullOrEmpty(request.IncludeItemTypes)) + return null; - return ((Folder)parentItem).GetItems(GetItemsQuery(request, user)); - } + var reportResult = await GetReportResult(request); - private InternalItemsQuery GetItemsQuery(BaseReportRequest request, User user) - { - var query = new InternalItemsQuery - { - User = user, - CollapseBoxSetItems = false - }; + return ToOptimizedResult(reportResult); + } - // Set query values based on request + /// <summary> Gets the given request. </summary> + /// <param name="request"> The request. </param> + /// <returns> A Task<object> </returns> + public async Task<object> Get(GetReportDownload request) + { + if (string.IsNullOrEmpty(request.IncludeItemTypes)) + return null; - // Example - //query.IncludeItemTypes = new[] {"Movie"}; + var headers = new Dictionary<string, string>(); + string fileExtension = "csv"; + string contentType = "text/plain;charset='utf-8'"; + switch (request.ExportType) + { + case ReportExportType.CSV: + break; + case ReportExportType.Excel: + contentType = "application/vnd.ms-excel"; + fileExtension = "xls"; + break; + } - return query; - } - } + var filename = "ReportExport." + fileExtension; + headers["Content-Disposition"] = string.Format("attachment; filename=\"{0}\"", filename); + headers["Content-Encoding"] = "UTF-8"; + + ReportViewType reportRowType = ReportHelper.GetRowType(request.IncludeItemTypes); + ReportBuilder reportBuilder = new ReportBuilder(_libraryManager); + QueryResult<BaseItem> queryResult = await GetQueryResult(request).ConfigureAwait(false); + ReportResult reportResult = reportBuilder.GetReportResult(queryResult.Items, reportRowType, request); + + reportResult.TotalRecordCount = queryResult.TotalRecordCount; + + string result = string.Empty; + switch (request.ExportType) + { + case ReportExportType.CSV: + result = new ReportExport().ExportToCsv(reportResult); + break; + case ReportExportType.Excel: + result = new ReportExport().ExportToExcel(reportResult); + break; + } + + object ro = ResultFactory.GetResult(result, contentType, headers); + return ro; + } + + /// <summary> Gets the given request. </summary> + /// <param name="request"> The request. </param> + /// <returns> A Task<object> </returns> + public async Task<object> Get(GetReportStatistics request) + { + if (string.IsNullOrEmpty(request.IncludeItemTypes)) + return null; + var reportResult = await GetReportStatistic(request); + + return ToOptimizedResult(reportResult); + } + + /// <summary> Gets report statistic. </summary> + /// <param name="request"> The request. </param> + /// <returns> The report statistic. </returns> + private async Task<ReportStatResult> GetReportStatistic(GetReportStatistics request) + { + ReportViewType reportRowType = ReportHelper.GetRowType(request.IncludeItemTypes); + QueryResult<BaseItem> queryResult = await GetQueryResult(request).ConfigureAwait(false); + + ReportStatBuilder reportBuilder = new ReportStatBuilder(_libraryManager); + ReportStatResult reportResult = reportBuilder.GetReportStatResult(queryResult.Items, ReportHelper.GetRowType(request.IncludeItemTypes), request.TopItems ?? 5); + reportResult.TotalRecordCount = reportResult.Groups.Count(); + return reportResult; + } + + /// <summary> Gets report result. </summary> + /// <param name="request"> The request. </param> + /// <returns> The report result. </returns> + private async Task<ReportResult> GetReportResult(GetItemReport request) + { + + ReportViewType reportRowType = ReportHelper.GetRowType(request.IncludeItemTypes); + ReportBuilder reportBuilder = new ReportBuilder(_libraryManager); + QueryResult<BaseItem> queryResult = await GetQueryResult(request).ConfigureAwait(false); + ReportResult reportResult = reportBuilder.GetReportResult(queryResult.Items, reportRowType, request); + reportResult.TotalRecordCount = queryResult.TotalRecordCount; + + return reportResult; + } + + /// <summary> Gets query result. </summary> + /// <param name="request"> The request. </param> + /// <returns> The query result. </returns> + private async Task<QueryResult<BaseItem>> GetQueryResult(BaseReportRequest request) + { + // Placeholder in case needed later + request.Recursive = true; + var user = request.UserId.HasValue ? _userManager.GetUserById(request.UserId.Value) : null; + request.Fields = "MediaSources,DateCreated,Settings,Studios,SyncInfo,ItemCounts"; + + var parentItem = string.IsNullOrEmpty(request.ParentId) ? + (user == null ? _libraryManager.RootFolder : user.RootFolder) : + _libraryManager.GetItemById(request.ParentId); + + var item = string.IsNullOrEmpty(request.ParentId) ? + user == null ? _libraryManager.RootFolder : user.RootFolder : + parentItem; + + IEnumerable<BaseItem> items; + + if (request.Recursive) + { + var result = await ((Folder)item).GetItems(GetItemsQuery(request, user)).ConfigureAwait(false); + return result; + } + else + { + if (user == null) + { + var result = await ((Folder)item).GetItems(GetItemsQuery(request, null)).ConfigureAwait(false); + return result; + } + + var userRoot = item as UserRootFolder; + + if (userRoot == null) + { + var result = await ((Folder)item).GetItems(GetItemsQuery(request, user)).ConfigureAwait(false); + + return result; + } + + items = ((Folder)item).GetChildren(user, true); + } + + return new QueryResult<BaseItem> { Items = items.ToArray() }; + + } + + /// <summary> Gets items query. </summary> + /// <param name="request"> The request. </param> + /// <param name="user"> The user. </param> + /// <returns> The items query. </returns> + private InternalItemsQuery GetItemsQuery(BaseReportRequest request, User user) + { + var query = new InternalItemsQuery + { + User = user, + IsPlayed = request.IsPlayed, + MediaTypes = request.GetMediaTypes(), + IncludeItemTypes = request.GetIncludeItemTypes(), + ExcludeItemTypes = request.GetExcludeItemTypes(), + Recursive = true, + SortBy = request.GetOrderBy(), + SortOrder = request.SortOrder ?? SortOrder.Ascending, + + Filter = i => ApplyAdditionalFilters(request, i, user, true, _libraryManager), + StartIndex = request.StartIndex, + IsMissing = request.IsMissing, + IsVirtualUnaired = request.IsVirtualUnaired, + IsUnaired = request.IsUnaired, + CollapseBoxSetItems = request.CollapseBoxSetItems, + NameLessThan = request.NameLessThan, + NameStartsWith = request.NameStartsWith, + NameStartsWithOrGreater = request.NameStartsWithOrGreater, + HasImdbId = request.HasImdbId, + IsYearMismatched = request.IsYearMismatched, + IsUnidentified = request.IsUnidentified, + IsPlaceHolder = request.IsPlaceHolder, + IsLocked = request.IsLocked, + IsInBoxSet = request.IsInBoxSet, + IsHD = request.IsHD, + Is3D = request.Is3D, + HasTvdbId = request.HasTvdbId, + HasTmdbId = request.HasTmdbId, + HasOverview = request.HasOverview, + HasOfficialRating = request.HasOfficialRating, + HasParentalRating = request.HasParentalRating, + HasSpecialFeature = request.HasSpecialFeature, + HasSubtitles = request.HasSubtitles, + HasThemeSong = request.HasThemeSong, + HasThemeVideo = request.HasThemeVideo, + HasTrailer = request.HasTrailer, + Tags = request.GetTags(), + OfficialRatings = request.GetOfficialRatings(), + Genres = request.GetGenres(), + Studios = request.GetStudios(), + StudioIds = request.GetStudioIds(), + Person = request.Person, + PersonIds = request.GetPersonIds(), + PersonTypes = request.GetPersonTypes(), + Years = request.GetYears(), + ImageTypes = request.GetImageTypes().ToArray(), + VideoTypes = request.GetVideoTypes().ToArray(), + AdjacentTo = request.AdjacentTo + }; + + if (!string.IsNullOrWhiteSpace(request.Ids)) + { + query.CollapseBoxSetItems = false; + } + + foreach (var filter in request.GetFilters()) + { + switch (filter) + { + case ItemFilter.Dislikes: + query.IsLiked = false; + break; + case ItemFilter.IsFavorite: + query.IsFavorite = true; + break; + case ItemFilter.IsFavoriteOrLikes: + query.IsFavoriteOrLiked = true; + break; + case ItemFilter.IsFolder: + query.IsFolder = true; + break; + case ItemFilter.IsNotFolder: + query.IsFolder = false; + break; + case ItemFilter.IsPlayed: + query.IsPlayed = true; + break; + case ItemFilter.IsRecentlyAdded: + break; + case ItemFilter.IsResumable: + query.IsResumable = true; + break; + case ItemFilter.IsUnplayed: + query.IsPlayed = false; + break; + case ItemFilter.Likes: + query.IsLiked = true; + break; + } + } + + if (request.HasQueryLimit) + query.Limit = request.Limit; + return query; + } + + /// <summary> Applies filtering. </summary> + /// <param name="items"> The items. </param> + /// <param name="filter"> The filter. </param> + /// <param name="user"> The user. </param> + /// <param name="repository"> The repository. </param> + /// <returns> IEnumerable{BaseItem}. </returns> + internal static IEnumerable<BaseItem> ApplyFilter(IEnumerable<BaseItem> items, ItemFilter filter, User user, IUserDataManager repository) + { + // Avoid implicitly captured closure + var currentUser = user; + + switch (filter) + { + case ItemFilter.IsFavoriteOrLikes: + return items.Where(item => + { + var userdata = repository.GetUserData(user.Id, item.GetUserDataKey()); + + if (userdata == null) + { + return false; + } + + var likes = userdata.Likes ?? false; + var favorite = userdata.IsFavorite; + + return likes || favorite; + }); + + case ItemFilter.Likes: + return items.Where(item => + { + var userdata = repository.GetUserData(user.Id, item.GetUserDataKey()); + + return userdata != null && userdata.Likes.HasValue && userdata.Likes.Value; + }); + + case ItemFilter.Dislikes: + return items.Where(item => + { + var userdata = repository.GetUserData(user.Id, item.GetUserDataKey()); + + return userdata != null && userdata.Likes.HasValue && !userdata.Likes.Value; + }); + + case ItemFilter.IsFavorite: + return items.Where(item => + { + var userdata = repository.GetUserData(user.Id, item.GetUserDataKey()); + + return userdata != null && userdata.IsFavorite; + }); + + case ItemFilter.IsResumable: + return items.Where(item => + { + var userdata = repository.GetUserData(user.Id, item.GetUserDataKey()); + + return userdata != null && userdata.PlaybackPositionTicks > 0; + }); + + case ItemFilter.IsPlayed: + return items.Where(item => item.IsPlayed(currentUser)); + + case ItemFilter.IsUnplayed: + return items.Where(item => item.IsUnplayed(currentUser)); + + case ItemFilter.IsFolder: + return items.Where(item => item.IsFolder); + + case ItemFilter.IsNotFolder: + return items.Where(item => !item.IsFolder); + + case ItemFilter.IsRecentlyAdded: + return items.Where(item => (DateTime.UtcNow - item.DateCreated).TotalDays <= 10); + } + + return items; + } + + /// <summary> Applies the additional filters. </summary> + /// <param name="request"> The request. </param> + /// <param name="i"> Zero-based index of the. </param> + /// <param name="user"> The user. </param> + /// <param name="isPreFiltered"> true if this object is pre filtered. </param> + /// <param name="libraryManager"> Manager for library. </param> + /// <returns> true if it succeeds, false if it fails. </returns> + private bool ApplyAdditionalFilters(BaseReportRequest request, BaseItem i, User user, bool isPreFiltered, ILibraryManager libraryManager) + { + var video = i as Video; + + if (!isPreFiltered) + { + var mediaTypes = request.GetMediaTypes(); + if (mediaTypes.Length > 0) + { + if (!(!string.IsNullOrEmpty(i.MediaType) && mediaTypes.Contains(i.MediaType, StringComparer.OrdinalIgnoreCase))) + { + return false; + } + } + + if (request.IsPlayed.HasValue) + { + var val = request.IsPlayed.Value; + if (i.IsPlayed(user) != val) + { + return false; + } + } + + // Exclude item types + var excluteItemTypes = request.GetExcludeItemTypes(); + if (excluteItemTypes.Length > 0 && excluteItemTypes.Contains(i.GetType().Name, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + // Include item types + var includeItemTypes = request.GetIncludeItemTypes(); + if (includeItemTypes.Length > 0 && !includeItemTypes.Contains(i.GetType().Name, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + if (request.IsInBoxSet.HasValue) + { + var val = request.IsInBoxSet.Value; + if (i.Parents.OfType<BoxSet>().Any() != val) + { + return false; + } + } + + // Filter by Video3DFormat + if (request.Is3D.HasValue) + { + var val = request.Is3D.Value; + + if (video == null || val != video.Video3DFormat.HasValue) + { + return false; + } + } + + if (request.IsHD.HasValue) + { + var val = request.IsHD.Value; + + if (video == null || val != video.IsHD) + { + return false; + } + } + + if (request.IsUnidentified.HasValue) + { + var val = request.IsUnidentified.Value; + if (i.IsUnidentified != val) + { + return false; + } + } + + if (request.IsLocked.HasValue) + { + var val = request.IsLocked.Value; + if (i.IsLocked != val) + { + return false; + } + } + + if (request.HasOverview.HasValue) + { + var filterValue = request.HasOverview.Value; + + var hasValue = !string.IsNullOrEmpty(i.Overview); + + if (hasValue != filterValue) + { + return false; + } + } + + if (request.HasImdbId.HasValue) + { + var filterValue = request.HasImdbId.Value; + + var hasValue = !string.IsNullOrEmpty(i.GetProviderId(MetadataProviders.Imdb)); + + if (hasValue != filterValue) + { + return false; + } + } + + if (request.HasTmdbId.HasValue) + { + var filterValue = request.HasTmdbId.Value; + + var hasValue = !string.IsNullOrEmpty(i.GetProviderId(MetadataProviders.Tmdb)); + + if (hasValue != filterValue) + { + return false; + } + } + + if (request.HasTvdbId.HasValue) + { + var filterValue = request.HasTvdbId.Value; + + var hasValue = !string.IsNullOrEmpty(i.GetProviderId(MetadataProviders.Tvdb)); + + if (hasValue != filterValue) + { + return false; + } + } + + if (request.IsYearMismatched.HasValue) + { + var filterValue = request.IsYearMismatched.Value; + + if (UserViewBuilder.IsYearMismatched(i, libraryManager) != filterValue) + { + return false; + } + } + + if (request.HasOfficialRating.HasValue) + { + var filterValue = request.HasOfficialRating.Value; + + var hasValue = !string.IsNullOrEmpty(i.OfficialRating); + + if (hasValue != filterValue) + { + return false; + } + } + + if (request.IsPlaceHolder.HasValue) + { + var filterValue = request.IsPlaceHolder.Value; + + var isPlaceHolder = false; + + var hasPlaceHolder = i as ISupportsPlaceHolders; + + if (hasPlaceHolder != null) + { + isPlaceHolder = hasPlaceHolder.IsPlaceHolder; + } + + if (isPlaceHolder != filterValue) + { + return false; + } + } + + if (request.HasSpecialFeature.HasValue) + { + var filterValue = request.HasSpecialFeature.Value; + + var movie = i as IHasSpecialFeatures; + + if (movie != null) + { + var ok = filterValue + ? movie.SpecialFeatureIds.Count > 0 + : movie.SpecialFeatureIds.Count == 0; + + if (!ok) + { + return false; + } + } + else + { + return false; + } + } + + if (request.HasSubtitles.HasValue) + { + var val = request.HasSubtitles.Value; + + if (video == null || val != video.HasSubtitles) + { + return false; + } + } + + if (request.HasParentalRating.HasValue) + { + var val = request.HasParentalRating.Value; + + var rating = i.CustomRating; + + if (string.IsNullOrEmpty(rating)) + { + rating = i.OfficialRating; + } + + if (val) + { + if (string.IsNullOrEmpty(rating)) + { + return false; + } + } + else + { + if (!string.IsNullOrEmpty(rating)) + { + return false; + } + } + } + + if (request.HasTrailer.HasValue) + { + var val = request.HasTrailer.Value; + var trailerCount = 0; + + var hasTrailers = i as IHasTrailers; + if (hasTrailers != null) + { + trailerCount = hasTrailers.GetTrailerIds().Count; + } + + var ok = val ? trailerCount > 0 : trailerCount == 0; + + if (!ok) + { + return false; + } + } + + if (request.HasThemeSong.HasValue) + { + var filterValue = request.HasThemeSong.Value; + + var themeCount = 0; + var iHasThemeMedia = i as IHasThemeMedia; + + if (iHasThemeMedia != null) + { + themeCount = iHasThemeMedia.ThemeSongIds.Count; + } + var ok = filterValue ? themeCount > 0 : themeCount == 0; + + if (!ok) + { + return false; + } + } + + if (request.HasThemeVideo.HasValue) + { + var filterValue = request.HasThemeVideo.Value; + + var themeCount = 0; + var iHasThemeMedia = i as IHasThemeMedia; + + if (iHasThemeMedia != null) + { + themeCount = iHasThemeMedia.ThemeVideoIds.Count; + } + var ok = filterValue ? themeCount > 0 : themeCount == 0; + + if (!ok) + { + return false; + } + } + + // Apply tag filter + var tags = request.GetTags(); + if (tags.Length > 0) + { + var hasTags = i as IHasTags; + if (hasTags == null) + { + return false; + } + if (!(tags.Any(v => hasTags.Tags.Contains(v, StringComparer.OrdinalIgnoreCase)))) + { + return false; + } + } + + // Apply official rating filter + var officialRatings = request.GetOfficialRatings(); + if (officialRatings.Length > 0 && !officialRatings.Contains(i.OfficialRating ?? string.Empty)) + { + return false; + } + + // Apply genre filter + var genres = request.GetGenres(); + if (genres.Length > 0 && !(genres.Any(v => i.Genres.Contains(v, StringComparer.OrdinalIgnoreCase)))) + { + return false; + } + + // Filter by VideoType + var videoTypes = request.GetVideoTypes(); + if (videoTypes.Length > 0 && (video == null || !videoTypes.Contains(video.VideoType))) + { + return false; + } + + var imageTypes = request.GetImageTypes().ToList(); + if (imageTypes.Count > 0) + { + if (!(imageTypes.Any(i.HasImage))) + { + return false; + } + } + + // Apply studio filter + var studios = request.GetStudios(); + if (studios.Length > 0 && !studios.Any(v => i.Studios.Contains(v, StringComparer.OrdinalIgnoreCase))) + { + return false; + } + + // Apply studio filter + var studioIds = request.GetStudioIds(); + if (studioIds.Length > 0 && !studioIds.Any(id => + { + var studioItem = libraryManager.GetItemById(id); + return studioItem != null && i.Studios.Contains(studioItem.Name, StringComparer.OrdinalIgnoreCase); + })) + { + return false; + } + + // Apply year filter + var years = request.GetYears(); + if (years.Length > 0 && !(i.ProductionYear.HasValue && years.Contains(i.ProductionYear.Value))) + { + return false; + } + + // Apply person filter + var personIds = request.GetPersonIds(); + if (personIds.Length > 0) + { + var names = personIds + .Select(libraryManager.GetItemById) + .Select(p => p == null ? "-1" : p.Name) + .ToList(); + + if (!(names.Any(v => i.People.Select(p => p.Name).Contains(v, StringComparer.OrdinalIgnoreCase)))) + { + return false; + } + } + + // Apply person filter + if (!string.IsNullOrEmpty(request.Person)) + { + var personTypes = request.GetPersonTypes(); + + if (personTypes.Length == 0) + { + if (!(i.People.Any(p => string.Equals(p.Name, request.Person, StringComparison.OrdinalIgnoreCase)))) + { + return false; + } + } + else + { + var types = personTypes; + + var ok = new[] { i }.Any(item => + item.People != null && + item.People.Any(p => + p.Name.Equals(request.Person, StringComparison.OrdinalIgnoreCase) && (types.Contains(p.Type, StringComparer.OrdinalIgnoreCase) || types.Contains(p.Role, StringComparer.OrdinalIgnoreCase)))); + + if (!ok) + { + return false; + } + } + } + } + + if (request.MinCommunityRating.HasValue) + { + var val = request.MinCommunityRating.Value; + + if (!(i.CommunityRating.HasValue && i.CommunityRating >= val)) + { + return false; + } + } + + if (request.MinCriticRating.HasValue) + { + var val = request.MinCriticRating.Value; + + var hasCriticRating = i as IHasCriticRating; + + if (hasCriticRating != null) + { + if (!(hasCriticRating.CriticRating.HasValue && hasCriticRating.CriticRating >= val)) + { + return false; + } + } + else + { + return false; + } + } + + // Artists + if (!string.IsNullOrEmpty(request.ArtistIds)) + { + var artistIds = request.ArtistIds.Split('|'); + + var audio = i as IHasArtist; + + if (!(audio != null && artistIds.Any(id => + { + var artistItem = libraryManager.GetItemById(id); + return artistItem != null && audio.HasAnyArtist(artistItem.Name); + }))) + { + return false; + } + } + + // Artists + if (!string.IsNullOrEmpty(request.Artists)) + { + var artists = request.Artists.Split('|'); + + var audio = i as IHasArtist; + + if (!(audio != null && artists.Any(audio.HasAnyArtist))) + { + return false; + } + } + + // Albums + if (!string.IsNullOrEmpty(request.Albums)) + { + var albums = request.Albums.Split('|'); + + var audio = i as Audio; + + if (audio != null) + { + if (!albums.Any(a => string.Equals(a, audio.Album, StringComparison.OrdinalIgnoreCase))) + { + return false; + } + } + + var album = i as MusicAlbum; + + if (album != null) + { + if (!albums.Any(a => string.Equals(a, album.Name, StringComparison.OrdinalIgnoreCase))) + { + return false; + } + } + + var musicVideo = i as MusicVideo; + + if (musicVideo != null) + { + if (!albums.Any(a => string.Equals(a, musicVideo.Album, StringComparison.OrdinalIgnoreCase))) + { + return false; + } + } + + return false; + } + + // Min index number + if (request.MinIndexNumber.HasValue) + { + if (!(i.IndexNumber.HasValue && i.IndexNumber.Value >= request.MinIndexNumber.Value)) + { + return false; + } + } + + // Min official rating + if (!string.IsNullOrEmpty(request.MinOfficialRating)) + { + var level = _localization.GetRatingLevel(request.MinOfficialRating); + + if (level.HasValue) + { + var rating = i.CustomRating; + + if (string.IsNullOrEmpty(rating)) + { + rating = i.OfficialRating; + } + + if (!string.IsNullOrEmpty(rating)) + { + var itemLevel = _localization.GetRatingLevel(rating); + + if (!(!itemLevel.HasValue || itemLevel.Value >= level.Value)) + { + return false; + } + } + } + } + + // Max official rating + if (!string.IsNullOrEmpty(request.MaxOfficialRating)) + { + var level = _localization.GetRatingLevel(request.MaxOfficialRating); + + if (level.HasValue) + { + var rating = i.CustomRating; + + if (string.IsNullOrEmpty(rating)) + { + rating = i.OfficialRating; + } + + if (!string.IsNullOrEmpty(rating)) + { + var itemLevel = _localization.GetRatingLevel(rating); + + if (!(!itemLevel.HasValue || itemLevel.Value <= level.Value)) + { + return false; + } + } + } + } + + // LocationTypes + if (!string.IsNullOrEmpty(request.LocationTypes)) + { + var vals = request.LocationTypes.Split(','); + if (!vals.Contains(i.LocationType.ToString(), StringComparer.OrdinalIgnoreCase)) + { + return false; + } + } + + // ExcludeLocationTypes + if (!string.IsNullOrEmpty(request.ExcludeLocationTypes)) + { + var vals = request.ExcludeLocationTypes.Split(','); + if (vals.Contains(i.LocationType.ToString(), StringComparer.OrdinalIgnoreCase)) + { + return false; + } + } + + if (!string.IsNullOrEmpty(request.AlbumArtistStartsWithOrGreater)) + { + var ok = new[] { i }.OfType<IHasAlbumArtist>() + .Any(p => string.Compare(request.AlbumArtistStartsWithOrGreater, p.AlbumArtists.FirstOrDefault(), StringComparison.CurrentCultureIgnoreCase) < 1); + + if (!ok) + { + return false; + } + } + + // Filter by Series Status + if (!string.IsNullOrEmpty(request.SeriesStatus)) + { + var vals = request.SeriesStatus.Split(','); + + var ok = new[] { i }.OfType<Series>().Any(p => p.Status.HasValue && vals.Contains(p.Status.Value.ToString(), StringComparer.OrdinalIgnoreCase)); + + if (!ok) + { + return false; + } + } + + // Filter by Series AirDays + if (!string.IsNullOrEmpty(request.AirDays)) + { + var days = request.AirDays.Split(',').Select(d => (DayOfWeek)Enum.Parse(typeof(DayOfWeek), d, true)); + + var ok = new[] { i }.OfType<Series>().Any(p => p.AirDays != null && days.Any(d => p.AirDays.Contains(d))); + + if (!ok) + { + return false; + } + } + + if (request.MinPlayers.HasValue) + { + var filterValue = request.MinPlayers.Value; + + var game = i as Game; + + if (game != null) + { + var players = game.PlayersSupported ?? 1; + + var ok = players >= filterValue; + + if (!ok) + { + return false; + } + } + else + { + return false; + } + } + + if (request.MaxPlayers.HasValue) + { + var filterValue = request.MaxPlayers.Value; + + var game = i as Game; + + if (game != null) + { + var players = game.PlayersSupported ?? 1; + + var ok = players <= filterValue; + + if (!ok) + { + return false; + } + } + else + { + return false; + } + } + + if (request.ParentIndexNumber.HasValue) + { + var filterValue = request.ParentIndexNumber.Value; + + var episode = i as Episode; + + if (episode != null) + { + if (episode.ParentIndexNumber.HasValue && episode.ParentIndexNumber.Value != filterValue) + { + return false; + } + } + + var song = i as Audio; + + if (song != null) + { + if (song.ParentIndexNumber.HasValue && song.ParentIndexNumber.Value != filterValue) + { + return false; + } + } + } + + if (request.AiredDuringSeason.HasValue) + { + var episode = i as Episode; + + if (episode == null) + { + return false; + } + + if (!Series.FilterEpisodesBySeason(new[] { episode }, request.AiredDuringSeason.Value, true).Any()) + { + return false; + } + } + + if (!string.IsNullOrEmpty(request.MinPremiereDate)) + { + var date = DateTime.Parse(request.MinPremiereDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); + + if (!(i.PremiereDate.HasValue && i.PremiereDate.Value >= date)) + { + return false; + } + } + + if (!string.IsNullOrEmpty(request.MaxPremiereDate)) + { + var date = DateTime.Parse(request.MaxPremiereDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); + + if (!(i.PremiereDate.HasValue && i.PremiereDate.Value <= date)) + { + return false; + } + } + + return true; + } + + /// <summary> Applies the paging. </summary> + /// <param name="request"> The request. </param> + /// <param name="items"> The items. </param> + /// <returns> IEnumerable{BaseItem}. </returns> + private IEnumerable<BaseItem> ApplyPaging(GetItems request, IEnumerable<BaseItem> items) + { + // Start at + if (request.StartIndex.HasValue) + { + items = items.Skip(request.StartIndex.Value); + } + + // Return limit + if (request.Limit.HasValue) + { + items = items.Take(request.Limit.Value); + } + + return items; + } + + } } diff --git a/MediaBrowser.Api/Reports/Stat/ReportStatBuilder.cs b/MediaBrowser.Api/Reports/Stat/ReportStatBuilder.cs new file mode 100644 index 000000000..e297a2a57 --- /dev/null +++ b/MediaBrowser.Api/Reports/Stat/ReportStatBuilder.cs @@ -0,0 +1,214 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.Reports +{ + /// <summary> A report stat builder. </summary> + /// <seealso cref="T:MediaBrowser.Api.Reports.ReportBuilderBase"/> + public class ReportStatBuilder : ReportBuilderBase + { + /// <summary> + /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportStatBuilder class. </summary> + /// <param name="libraryManager"> Manager for library. </param> + public ReportStatBuilder(ILibraryManager libraryManager) + : base(libraryManager) + { + } + + /// <summary> Gets report stat result. </summary> + /// <param name="items"> The items. </param> + /// <param name="reportRowType"> Type of the report row. </param> + /// <param name="topItem"> The top item. </param> + /// <returns> The report stat result. </returns> + public ReportStatResult GetReportStatResult(BaseItem[] items, ReportViewType reportRowType, int topItem = 5) + { + ReportStatResult result = new ReportStatResult(); + result = this.GetResultGenres(result, items, topItem); + result = this.GetResultStudios(result, items, topItem); + result = this.GetResultPersons(result, items, topItem); + result = this.GetResultProductionYears(result, items, topItem); + result = this.GetResulProductionLocations(result, items, topItem); + result = this.GetResultCommunityRatings(result, items, topItem); + result = this.GetResultParentalRatings(result, items, topItem); + + switch (reportRowType) + { + case ReportViewType.Season: + case ReportViewType.Series: + case ReportViewType.MusicAlbum: + case ReportViewType.MusicArtist: + case ReportViewType.Game: + break; + case ReportViewType.Movie: + case ReportViewType.BoxSet: + + break; + case ReportViewType.Book: + case ReportViewType.Episode: + case ReportViewType.Video: + case ReportViewType.MusicVideo: + case ReportViewType.Trailer: + case ReportViewType.Audio: + case ReportViewType.BaseItem: + default: + break; + } + + result.Groups = result.Groups.OrderByDescending(n => n.Items.Count()).ToList(); + + return result; + } + + private ReportStatResult GetResultGenres(ReportStatResult result, BaseItem[] items, int topItem = 5) + { + this.GetGroups(result, ReportHelper.GetServerLocalizedString("HeaderGenres"), topItem, + items.SelectMany(x => x.Genres) + .GroupBy(x => x) + .OrderByDescending(x => x.Count()) + .Take(topItem) + .Select(x => new ReportStatItem + { + Name = x.Key, + Value = x.Count().ToString(), + Id = GetGenreID(x.Key) + })); + return result; + + } + + private ReportStatResult GetResultStudios(ReportStatResult result, BaseItem[] items, int topItem = 5) + { + this.GetGroups(result, ReportHelper.GetServerLocalizedString("HeaderStudios"), topItem, + items.SelectMany(x => x.Studios) + .GroupBy(x => x) + .OrderByDescending(x => x.Count()) + .Take(topItem) + .Select(x => new ReportStatItem + { + Name = x.Key, + Value = x.Count().ToString(), + Id = GetStudioID(x.Key) + }) + ); + + return result; + + } + + private ReportStatResult GetResultPersons(ReportStatResult result, BaseItem[] items, int topItem = 5) + { + List<string> t = new List<string> { PersonType.Actor, PersonType.Composer, PersonType.Director, PersonType.GuestStar, PersonType.Producer, PersonType.Writer, "Artist", "AlbumArtist" }; + foreach (var item in t) + { + this.GetGroups(result, ReportHelper.GetServerLocalizedString("Option" + item), topItem, + items.SelectMany(x => x.People) + .Where(n => n.Type == item) + .GroupBy(x => x.Name) + .OrderByDescending(x => x.Count()) + .Take(topItem) + .Select(x => new ReportStatItem + { + Name = x.Key, + Value = x.Count().ToString(), + Id = GetPersonID(x.Key) + }) + ); + } + + return result; + } + + private ReportStatResult GetResultCommunityRatings(ReportStatResult result, BaseItem[] items, int topItem = 5) + { + this.GetGroups(result, ReportHelper.GetServerLocalizedString("LabelCommunityRating"), topItem, + items.Where(x => x.CommunityRating != null && x.CommunityRating > 0) + .GroupBy(x => x.CommunityRating) + .OrderByDescending(x => x.Count()) + .Take(topItem) + .Select(x => new ReportStatItem + { + Name = x.Key.ToString(), + Value = x.Count().ToString() + }) + ); + + return result; + } + + private ReportStatResult GetResultParentalRatings(ReportStatResult result, BaseItem[] items, int topItem = 5) + { + this.GetGroups(result, ReportHelper.GetServerLocalizedString("HeaderParentalRatings"), topItem, + items.Where(x => x.OfficialRating != null) + .GroupBy(x => x.OfficialRating) + .OrderByDescending(x => x.Count()) + .Take(topItem) + .Select(x => new ReportStatItem + { + Name = x.Key.ToString(), + Value = x.Count().ToString() + }) + ); + + return result; + } + + + private ReportStatResult GetResultProductionYears(ReportStatResult result, BaseItem[] items, int topItem = 5) + { + this.GetGroups(result, ReportHelper.GetServerLocalizedString("HeaderYears"), topItem, + items.Where(x => x.ProductionYear != null && x.ProductionYear > 0) + .GroupBy(x => x.ProductionYear) + .OrderByDescending(x => x.Count()) + .Take(topItem) + .Select(x => new ReportStatItem + { + Name = x.Key.ToString(), + Value = x.Count().ToString() + }) + ); + + return result; + } + + private ReportStatResult GetResulProductionLocations(ReportStatResult result, BaseItem[] items, int topItem = 5) + { + this.GetGroups(result, ReportHelper.GetServerLocalizedString("HeaderCountries"), topItem, + items.OfType<IHasProductionLocations>() + .Where(x => x.ProductionLocations != null) + .SelectMany(x => x.ProductionLocations) + .GroupBy(x => x) + .OrderByDescending(x => x.Count()) + .Take(topItem) + .Select(x => new ReportStatItem + { + Name = x.Key.ToString(), + Value = x.Count().ToString() + }) + ); + + return result; + } + + + /// <summary> Gets the groups. </summary> + /// <param name="result"> The result. </param> + /// <param name="header"> The header. </param> + /// <param name="topItem"> The top item. </param> + /// <param name="top"> The top. </param> + private void GetGroups(ReportStatResult result, string header, int topItem, IEnumerable<ReportStatItem> top) + { + if (top.Count() > 0) + { + var group = new ReportStatGroup { Header = ReportStatGroup.FormatedHeader(header, topItem) }; + group.Items.AddRange(top); + result.Groups.Add(group); + } + } + } +} diff --git a/MediaBrowser.Api/Reports/Stat/ReportStatGroup.cs b/MediaBrowser.Api/Reports/Stat/ReportStatGroup.cs new file mode 100644 index 000000000..378eda935 --- /dev/null +++ b/MediaBrowser.Api/Reports/Stat/ReportStatGroup.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.Reports +{ + /// <summary> A report stat group. </summary> + public class ReportStatGroup + { + /// <summary> + /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportStatGroup class. </summary> + public ReportStatGroup() + { + Items = new List<ReportStatItem>(); + TotalRecordCount = 0; + } + + /// <summary> Gets or sets the header. </summary> + /// <value> The header. </value> + public string Header { get; set; } + + /// <summary> Gets or sets the items. </summary> + /// <value> The items. </value> + public List<ReportStatItem> Items { get; set; } + + /// <summary> Gets or sets the number of total records. </summary> + /// <value> The total number of record count. </value> + public int TotalRecordCount { get; set; } + + internal static string FormatedHeader(string header, int topItem) + { + return string.Format("Top {0} {1}", topItem, header); + } + } +} diff --git a/MediaBrowser.Api/Reports/Stat/ReportStatItem.cs b/MediaBrowser.Api/Reports/Stat/ReportStatItem.cs new file mode 100644 index 000000000..c7b14511f --- /dev/null +++ b/MediaBrowser.Api/Reports/Stat/ReportStatItem.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.Reports +{ + /// <summary> A report stat item. </summary> + public class ReportStatItem + { + /// <summary> Gets or sets the name. </summary> + /// <value> The name. </value> + public string Name { get; set; } + + /// <summary> Gets or sets the image. </summary> + /// <value> The image. </value> + public string Image { get; set; } + + /// <summary> Gets or sets the value. </summary> + /// <value> The value. </value> + public string Value { get; set; } + + /// <summary> Gets or sets the identifier. </summary> + /// <value> The identifier. </value> + public string Id { get; set; } + + } +} diff --git a/MediaBrowser.Api/Reports/Stat/ReportStatResult.cs b/MediaBrowser.Api/Reports/Stat/ReportStatResult.cs new file mode 100644 index 000000000..66d5f16a4 --- /dev/null +++ b/MediaBrowser.Api/Reports/Stat/ReportStatResult.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.Reports +{ + /// <summary> Encapsulates the result of a report stat. </summary> + public class ReportStatResult + { + /// <summary> + /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportStatResult class. </summary> + public ReportStatResult() + { + Groups = new List<ReportStatGroup>(); + TotalRecordCount = 0; + } + + /// <summary> Gets or sets the groups. </summary> + /// <value> The groups. </value> + public List<ReportStatGroup> Groups { get; set; } + + /// <summary> Gets or sets the number of total records. </summary> + /// <value> The total number of record count. </value> + public int TotalRecordCount { get; set; } + } +} |
