aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Api
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Api')
-rw-r--r--MediaBrowser.Api/ApiEntryPoint.cs34
-rw-r--r--MediaBrowser.Api/Dlna/DlnaServerService.cs2
-rw-r--r--MediaBrowser.Api/Images/ImageService.cs8
-rw-r--r--MediaBrowser.Api/ItemUpdateService.cs30
-rw-r--r--MediaBrowser.Api/MediaBrowser.Api.csproj24
-rw-r--r--MediaBrowser.Api/Music/InstantMixService.cs2
-rw-r--r--MediaBrowser.Api/PackageService.cs7
-rw-r--r--MediaBrowser.Api/Playback/BaseStreamingService.cs57
-rw-r--r--MediaBrowser.Api/Playback/Hls/BaseHlsService.cs7
-rw-r--r--MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs448
-rw-r--r--MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs35
-rw-r--r--MediaBrowser.Api/Playback/Hls/VideoHlsService.cs21
-rw-r--r--MediaBrowser.Api/Playback/Progressive/VideoService.cs2
-rw-r--r--MediaBrowser.Api/Playback/StreamState.cs7
-rw-r--r--MediaBrowser.Api/Playback/TranscodingThrottler.cs2
-rw-r--r--MediaBrowser.Api/PluginService.cs34
-rw-r--r--MediaBrowser.Api/Reports/Common/HeaderMetadata.cs47
-rw-r--r--MediaBrowser.Api/Reports/Common/ItemViewType.cs20
-rw-r--r--MediaBrowser.Api/Reports/Common/ReportBuilderBase.cs229
-rw-r--r--MediaBrowser.Api/Reports/Common/ReportExportType.cs12
-rw-r--r--MediaBrowser.Api/Reports/Common/ReportFieldType.cs19
-rw-r--r--MediaBrowser.Api/Reports/Common/ReportHeaderIdType.cs12
-rw-r--r--MediaBrowser.Api/Reports/Common/ReportHelper.cs101
-rw-r--r--MediaBrowser.Api/Reports/Common/ReportViewType.cs25
-rw-r--r--MediaBrowser.Api/Reports/Data/ReportBuilder.cs589
-rw-r--r--MediaBrowser.Api/Reports/Data/ReportExport.cs212
-rw-r--r--MediaBrowser.Api/Reports/Data/ReportGroup.cs44
-rw-r--r--MediaBrowser.Api/Reports/Data/ReportHeader.cs54
-rw-r--r--MediaBrowser.Api/Reports/Data/ReportItem.cs34
-rw-r--r--MediaBrowser.Api/Reports/Data/ReportOptions.cs52
-rw-r--r--MediaBrowser.Api/Reports/Data/ReportResult.cs53
-rw-r--r--MediaBrowser.Api/Reports/Data/ReportRow.cs71
-rw-r--r--MediaBrowser.Api/Reports/ReportFieldType.cs9
-rw-r--r--MediaBrowser.Api/Reports/ReportRequests.cs292
-rw-r--r--MediaBrowser.Api/Reports/ReportResult.cs16
-rw-r--r--MediaBrowser.Api/Reports/ReportsService.cs1183
-rw-r--r--MediaBrowser.Api/Reports/Stat/ReportStatBuilder.cs214
-rw-r--r--MediaBrowser.Api/Reports/Stat/ReportStatGroup.cs37
-rw-r--r--MediaBrowser.Api/Reports/Stat/ReportStatItem.cs29
-rw-r--r--MediaBrowser.Api/Reports/Stat/ReportStatResult.cs28
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) ? "&nbsp;" : 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 + "'>" + "&nbsp;" + "</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&lt;object&gt; </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&lt;object&gt; </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&lt;object&gt; </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&lt;object&gt; </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; }
+ }
+}