فهرست منبع

Merge pull request #1109 from MediaBrowser/dev

3.0.5621.3
Luke 10 سال پیش
والد
کامیت
8bd7055d17
100فایلهای تغییر یافته به همراه4827 افزوده شده و 1184 حذف شده
  1. 9 1
      Emby.Drawing/ImageMagick/ImageMagickEncoder.cs
  2. 16 10
      MediaBrowser.Api/ApiEntryPoint.cs
  3. 1 1
      MediaBrowser.Api/Dlna/DlnaServerService.cs
  4. 6 2
      MediaBrowser.Api/Images/ImageService.cs
  5. 18 12
      MediaBrowser.Api/ItemUpdateService.cs
  6. 21 3
      MediaBrowser.Api/MediaBrowser.Api.csproj
  7. 1 1
      MediaBrowser.Api/Music/InstantMixService.cs
  8. 7 0
      MediaBrowser.Api/PackageService.cs
  9. 40 17
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  10. 4 3
      MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
  11. 335 109
      MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
  12. 29 6
      MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs
  13. 1 20
      MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
  14. 1 1
      MediaBrowser.Api/Playback/Progressive/VideoService.cs
  15. 5 2
      MediaBrowser.Api/Playback/StreamState.cs
  16. 1 1
      MediaBrowser.Api/Playback/TranscodingThrottler.cs
  17. 30 4
      MediaBrowser.Api/PluginService.cs
  18. 47 0
      MediaBrowser.Api/Reports/Common/HeaderMetadata.cs
  19. 20 0
      MediaBrowser.Api/Reports/Common/ItemViewType.cs
  20. 229 0
      MediaBrowser.Api/Reports/Common/ReportBuilderBase.cs
  21. 12 0
      MediaBrowser.Api/Reports/Common/ReportExportType.cs
  22. 19 0
      MediaBrowser.Api/Reports/Common/ReportFieldType.cs
  23. 12 0
      MediaBrowser.Api/Reports/Common/ReportHeaderIdType.cs
  24. 101 0
      MediaBrowser.Api/Reports/Common/ReportHelper.cs
  25. 25 0
      MediaBrowser.Api/Reports/Common/ReportViewType.cs
  26. 589 0
      MediaBrowser.Api/Reports/Data/ReportBuilder.cs
  27. 212 0
      MediaBrowser.Api/Reports/Data/ReportExport.cs
  28. 44 0
      MediaBrowser.Api/Reports/Data/ReportGroup.cs
  29. 54 0
      MediaBrowser.Api/Reports/Data/ReportHeader.cs
  30. 34 0
      MediaBrowser.Api/Reports/Data/ReportItem.cs
  31. 52 0
      MediaBrowser.Api/Reports/Data/ReportOptions.cs
  32. 53 0
      MediaBrowser.Api/Reports/Data/ReportResult.cs
  33. 71 0
      MediaBrowser.Api/Reports/Data/ReportRow.cs
  34. 0 9
      MediaBrowser.Api/Reports/ReportFieldType.cs
  35. 273 19
      MediaBrowser.Api/Reports/ReportRequests.cs
  36. 0 16
      MediaBrowser.Api/Reports/ReportResult.cs
  37. 1141 42
      MediaBrowser.Api/Reports/ReportsService.cs
  38. 214 0
      MediaBrowser.Api/Reports/Stat/ReportStatBuilder.cs
  39. 37 0
      MediaBrowser.Api/Reports/Stat/ReportStatGroup.cs
  40. 29 0
      MediaBrowser.Api/Reports/Stat/ReportStatItem.cs
  41. 28 0
      MediaBrowser.Api/Reports/Stat/ReportStatResult.cs
  42. 2 1
      MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
  43. 22 1
      MediaBrowser.Controller/Providers/ItemInfo.cs
  44. 14 4
      MediaBrowser.Dlna/PlayTo/Device.cs
  45. 35 17
      MediaBrowser.Dlna/PlayTo/PlayToController.cs
  46. 2 2
      MediaBrowser.Dlna/Profiles/DefaultProfile.cs
  47. 1 1
      MediaBrowser.Dlna/Profiles/WdtvLiveProfile.cs
  48. 2 3
      MediaBrowser.Dlna/Profiles/Xml/BubbleUPnp.xml
  49. 2 3
      MediaBrowser.Dlna/Profiles/Xml/Default.xml
  50. 2 3
      MediaBrowser.Dlna/Profiles/Xml/Denon AVR.xml
  51. 2 3
      MediaBrowser.Dlna/Profiles/Xml/DirecTV HD-DVR.xml
  52. 2 3
      MediaBrowser.Dlna/Profiles/Xml/Dish Hopper-Joey.xml
  53. 0 23
      MediaBrowser.Dlna/Profiles/Xml/Generic Device.xml
  54. 2 3
      MediaBrowser.Dlna/Profiles/Xml/LG Smart TV.xml
  55. 2 3
      MediaBrowser.Dlna/Profiles/Xml/Linksys DMA2100.xml
  56. 2 3
      MediaBrowser.Dlna/Profiles/Xml/MediaMonkey.xml
  57. 2 3
      MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml
  58. 2 3
      MediaBrowser.Dlna/Profiles/Xml/Popcorn Hour.xml
  59. 2 3
      MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml
  60. 2 3
      MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml
  61. 2 3
      MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player.xml
  62. 2 3
      MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2010).xml
  63. 2 3
      MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2011).xml
  64. 2 3
      MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2012).xml
  65. 2 3
      MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2013).xml
  66. 2 3
      MediaBrowser.Dlna/Profiles/Xml/Sony PlayStation 3.xml
  67. 2 3
      MediaBrowser.Dlna/Profiles/Xml/Vlc.xml
  68. 3 4
      MediaBrowser.Dlna/Profiles/Xml/WDTV Live.xml
  69. 2 3
      MediaBrowser.Dlna/Profiles/Xml/Xbox 360.xml
  70. 2 3
      MediaBrowser.Dlna/Profiles/Xml/Xbox One.xml
  71. 2 3
      MediaBrowser.Dlna/Profiles/Xml/foobar2000.xml
  72. 1 1
      MediaBrowser.LocalMetadata/BaseXmlProvider.cs
  73. 3 25
      MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs
  74. 2 1
      MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs
  75. 33 4
      MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs
  76. 15 6
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  77. 4 39
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  78. 2 2
      MediaBrowser.Model/Configuration/EncodingOptions.cs
  79. 11 3
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  80. 1 1
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  81. 5 0
      MediaBrowser.Model/Dlna/StreamInfo.cs
  82. 6 0
      MediaBrowser.Model/Updates/PackageInfo.cs
  83. 1 1
      MediaBrowser.Providers/Manager/MetadataService.cs
  84. 1 1
      MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
  85. 1 1
      MediaBrowser.Providers/Music/FanArtAlbumProvider.cs
  86. 10 1
      MediaBrowser.Server.Implementations/Devices/DeviceManager.cs
  87. 20 9
      MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs
  88. 2 2
      MediaBrowser.Server.Implementations/HttpServer/Security/SessionContext.cs
  89. 3 3
      MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs
  90. 1 0
      MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
  91. 26 6
      MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
  92. 35 12
      MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
  93. 6 2
      MediaBrowser.Server.Implementations/Localization/JavaScript/ar.json
  94. 6 2
      MediaBrowser.Server.Implementations/Localization/JavaScript/be-BY.json
  95. 6 2
      MediaBrowser.Server.Implementations/Localization/JavaScript/bg-BG.json
  96. 6 2
      MediaBrowser.Server.Implementations/Localization/JavaScript/ca.json
  97. 6 2
      MediaBrowser.Server.Implementations/Localization/JavaScript/cs.json
  98. 648 644
      MediaBrowser.Server.Implementations/Localization/JavaScript/da.json
  99. 16 12
      MediaBrowser.Server.Implementations/Localization/JavaScript/de.json
  100. 6 2
      MediaBrowser.Server.Implementations/Localization/JavaScript/el.json

+ 9 - 1
Emby.Drawing/ImageMagick/ImageMagickEncoder.cs

@@ -115,9 +115,17 @@ namespace Emby.Drawing.ImageMagick
             }
         }
 
+        private bool HasTransparency(string path)
+        {
+            var ext = Path.GetExtension(path);
+
+            return string.Equals(ext, ".png", StringComparison.OrdinalIgnoreCase) ||
+                string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase);
+        }
+
         public void EncodeImage(string inputPath, string outputPath, int width, int height, int quality, ImageProcessingOptions options)
         {
-            if (string.IsNullOrWhiteSpace(options.BackgroundColor))
+            if (string.IsNullOrWhiteSpace(options.BackgroundColor) || !HasTransparency(inputPath))
             {
                 using (var originalImage = new MagickWand(inputPath))
                 {

+ 16 - 10
MediaBrowser.Api/ApiEntryPoint.cs

@@ -250,19 +250,19 @@ namespace MediaBrowser.Api
             return GetTranscodingJob(path, type) != null;
         }
 
-        public TranscodingJob GetTranscodingJob(string path, TranscodingJobType type)
+        public TranscodingJob GetTranscodingJobByPlaySessionId(string playSessionId)
         {
             lock (_activeTranscodingJobs)
             {
-                return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
+                return _activeTranscodingJobs.FirstOrDefault(j => j.PlaySessionId.Equals(playSessionId, StringComparison.OrdinalIgnoreCase));
             }
         }
 
-        public TranscodingJob GetTranscodingJob(string id)
+        public TranscodingJob GetTranscodingJob(string path, TranscodingJobType type)
         {
             lock (_activeTranscodingJobs)
             {
-                return _activeTranscodingJobs.FirstOrDefault(j => j.Id.Equals(id, StringComparison.OrdinalIgnoreCase));
+                return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
             }
         }
 
@@ -339,14 +339,17 @@ namespace MediaBrowser.Api
                 return;
             }
 
-            var timerDuration = job.Type == TranscodingJobType.Progressive ?
-                1000 :
-                1800000;
+            var timerDuration = 1000;
 
-            // We can really reduce the timeout for apps that are using the newer api
-            if (!string.IsNullOrWhiteSpace(job.PlaySessionId) && job.Type != TranscodingJobType.Progressive)
+            if (job.Type != TranscodingJobType.Progressive)
             {
-                timerDuration = 50000;
+                timerDuration = 1800000;
+                
+                // We can really reduce the timeout for apps that are using the newer api
+                if (!string.IsNullOrWhiteSpace(job.PlaySessionId))
+                {
+                    timerDuration = 60000;
+                }
             }
 
             job.PingTimeout = timerDuration;
@@ -628,6 +631,9 @@ namespace MediaBrowser.Api
         /// </summary>
         /// <value>The live stream identifier.</value>
         public string LiveStreamId { get; set; }
+
+        public bool IsLiveOutput { get; set; }
+
         /// <summary>
         /// Gets or sets the path.
         /// </summary>

+ 1 - 1
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)
         {

+ 6 - 2
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;
             }

+ 18 - 12
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;

+ 21 - 3
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" />

+ 1 - 1
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; }
     }
 

+ 7 - 0
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());
         }
 

+ 40 - 17
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,

+ 4 - 3
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

+ 335 - 109
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 GetAsync(request, "GET");
+            return GetMasterPlaylistInternal(request, "HEAD");
         }
 
-        public Task<object> Head(GetMasterHlsVideoStream request)
+        public Task<object> Get(GetMasterHlsAudioPlaylist request)
         {
-            return GetAsync(request, "HEAD");
+            return GetMasterPlaylistInternal(request, "GET");
         }
 
-        public Task<object> Get(GetMainHlsVideoStream request)
+        public Task<object> Head(GetMasterHlsAudioPlaylist request)
         {
-            return GetPlaylistAsync(request, "main");
+            return GetMasterPlaylistInternal(request, "HEAD");
+        }
+
+        public Task<object> Get(GetVariantHlsVideoPlaylist request)
+        {
+            return GetVariantPlaylistInternal(request, true, "main");
+        }
+
+        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(GetDynamicHlsVideoSegment request)
+        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);
@@ -130,7 +185,7 @@ namespace MediaBrowser.Api.Playback.Hls
                 {
                     var startTranscoding = false;
 
-                    var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
+                    var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, request.PlaySessionId, segmentExtension);
                     var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
 
                     if (currentTranscodingIndex == null)
@@ -155,12 +210,14 @@ namespace MediaBrowser.Api.Playback.Hls
                         {
                             ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.PlaySessionId, p => false);
 
+                            await ReadSegmentLengths(playlistPath).ConfigureAwait(false);
+
                             if (currentTranscodingIndex.HasValue)
                             {
                                 DeleteLastFile(playlistPath, segmentExtension, 0);
                             }
 
-                            request.StartTimeTicks = GetSeekPositionTicks(state, requestedIndex);
+                            request.StartTimeTicks = GetSeekPositionTicks(state, playlistPath, requestedIndex);
 
                             job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
                         }
@@ -187,28 +244,92 @@ namespace MediaBrowser.Api.Playback.Hls
                 ApiEntryPoint.Instance.TranscodingStartLock.Release();
             }
 
-            Logger.Info("waiting for {0}", segmentPath);
-            while (!File.Exists(segmentPath))
-            {
-                await Task.Delay(50, cancellationToken).ConfigureAwait(false);
-            }
+            //Logger.Info("waiting for {0}", segmentPath);
+            //while (!File.Exists(segmentPath))
+            //{
+            //    await Task.Delay(50, cancellationToken).ConfigureAwait(false);
+            //}
 
             Logger.Info("returning {0}", segmentPath);
             job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
             return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false);
         }
 
-        private long GetSeekPositionTicks(StreamState state, int requestedIndex)
+        private static readonly ConcurrentDictionary<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;
         }
 
-        public int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
+        public int? GetCurrentTranscodingIndex(string playlist, string playSessionId, string segmentExtension)
         {
-            var job = ApiEntryPoint.Instance.GetTranscodingJob(playlist, TranscodingJobType);
+            var job = string.IsNullOrWhiteSpace(playSessionId) ?
+                ApiEntryPoint.Instance.GetTranscodingJob(playlist, TranscodingJobType) :
+                ApiEntryPoint.Instance.GetTranscodingJobByPlaySessionId(playSessionId);
 
             if (job == null || job.HasExited)
             {
@@ -292,7 +413,7 @@ namespace MediaBrowser.Api.Playback.Hls
         {
             var segmentId = "0";
 
-            var segmentRequest = request as GetDynamicHlsVideoSegment;
+            var segmentRequest = request as GetHlsVideoSegment;
             if (segmentRequest != null)
             {
                 segmentId = segmentRequest.SegmentId;
@@ -301,13 +422,13 @@ namespace MediaBrowser.Api.Playback.Hls
             return int.Parse(segmentId, NumberStyles.Integer, UsCulture);
         }
 
-        private string GetSegmentPath(string playlist, int index)
+        private string GetSegmentPath(StreamState state, string playlist, int index)
         {
             var folder = Path.GetDirectoryName(playlist);
 
             var filename = Path.GetFileNameWithoutExtension(playlist);
 
-            return Path.Combine(folder, filename + index.ToString(UsCulture) + ".ts");
+            return Path.Combine(folder, filename + index.ToString(UsCulture) + GetSegmentFileExtension(state));
         }
 
         private async Task<object> GetSegmentResult(string playlistPath,
@@ -325,21 +446,26 @@ namespace MediaBrowser.Api.Playback.Hls
 
             var segmentFilename = Path.GetFileName(segmentPath);
 
-            using (var fileStream = GetPlaylistFileStream(playlistPath))
+            while (!cancellationToken.IsCancellationRequested)
             {
-                using (var reader = new StreamReader(fileStream))
+                using (var fileStream = GetPlaylistFileStream(playlistPath))
                 {
-                    while (!reader.EndOfStream)
+                    using (var reader = new StreamReader(fileStream))
                     {
-                        var text = await reader.ReadLineAsync().ConfigureAwait(false);
-
-                        // If it appears in the playlist, it's done
-                        if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1)
+                        while (!reader.EndOfStream)
                         {
-                            return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob);
+                            var text = await reader.ReadLineAsync().ConfigureAwait(false);
+
+                            // If it appears in the playlist, it's done
+                            if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1)
+                            {
+                                return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob);
+                            }
                         }
                     }
                 }
+
+                await Task.Delay(100, cancellationToken).ConfigureAwait(false);
             }
 
             // if a different file is encoding, it's done
@@ -349,34 +475,35 @@ namespace MediaBrowser.Api.Playback.Hls
             //return GetSegmentResult(segmentPath, segmentIndex);
             //}
 
-            // Wait for the file to stop being written to, then stream it
-            var length = new FileInfo(segmentPath).Length;
-            var eofCount = 0;
-
-            while (eofCount < 10)
-            {
-                var info = new FileInfo(segmentPath);
-
-                if (!info.Exists)
-                {
-                    break;
-                }
-
-                var newLength = info.Length;
+            //// Wait for the file to stop being written to, then stream it
+            //var length = new FileInfo(segmentPath).Length;
+            //var eofCount = 0;
 
-                if (newLength == length)
-                {
-                    eofCount++;
-                }
-                else
-                {
-                    eofCount = 0;
-                }
-
-                length = newLength;
-                await Task.Delay(100, cancellationToken).ConfigureAwait(false);
-            }
+            //while (eofCount < 10)
+            //{
+            //    var info = new FileInfo(segmentPath);
+
+            //    if (!info.Exists)
+            //    {
+            //        break;
+            //    }
+
+            //    var newLength = info.Length;
+
+            //    if (newLength == length)
+            //    {
+            //        eofCount++;
+            //    }
+            //    else
+            //    {
+            //        eofCount = 0;
+            //    }
+
+            //    length = newLength;
+            //    await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+            //}
 
+            cancellationToken.ThrowIfCancellationRequested();
             return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob);
         }
 
@@ -400,7 +527,7 @@ namespace MediaBrowser.Api.Playback.Hls
             });
         }
 
-        private async Task<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 +564,16 @@ namespace MediaBrowser.Api.Playback.Hls
             var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
             playlistUrl += queryString;
 
-            var request = (GetMasterHlsVideoStream)state.Request;
+            var request = state.Request;
 
             var subtitleStreams = state.MediaSource
                 .MediaStreams
                 .Where(i => i.IsTextSubtitleStream)
                 .ToList();
 
-            var subtitleGroup = subtitleStreams.Count > 0 && request.SubtitleMethod == SubtitleDeliveryMethod.Hls ?
+            var subtitleGroup = subtitleStreams.Count > 0 &&
+                (request is GetMasterHlsVideoPlaylist) &&
+                ((GetMasterHlsVideoPlaylist)request).SubtitleMethod == SubtitleDeliveryMethod.Hls ?
                 "subs" :
                 null;
 
@@ -452,7 +581,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
             if (EnableAdaptiveBitrateStreaming(state, isLiveStream))
             {
-                var requestedVideoBitrate = state.VideoRequest.VideoBitRate.Value;
+                var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
 
                 // By default, vary by just 200k
                 var variation = GetBitrateVariation(totalBitrate);
@@ -522,7 +651,7 @@ namespace MediaBrowser.Api.Playback.Hls
                 return false;
             }
 
-            var request = state.Request as GetMasterHlsVideoStream;
+            var request = state.Request as IMasterHlsRequest;
             if (request != null && !request.EnableAdaptiveBitrateStreaming)
             {
                 return false;
@@ -544,6 +673,11 @@ namespace MediaBrowser.Api.Playback.Hls
                 return false;
             }
 
+            if (!state.IsOutputVideo)
+            {
+                return false;
+            }
+
             // Having problems in android
             return false;
             //return state.VideoRequest.VideoBitRate.HasValue;
@@ -599,7 +733,7 @@ namespace MediaBrowser.Api.Playback.Hls
             return variation;
         }
 
-        private async Task<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 +741,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
             builder.AppendLine("#EXTM3U");
             builder.AppendLine("#EXT-X-VERSION:3");
-            builder.AppendLine("#EXT-X-TARGETDURATION:" + state.SegmentLength.ToString(UsCulture));
+            builder.AppendLine("#EXT-X-TARGETDURATION:" + (state.SegmentLength).ToString(UsCulture));
             builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
 
             var queryStringIndex = Request.RawUrl.IndexOf('?');
@@ -623,10 +757,11 @@ namespace MediaBrowser.Api.Playback.Hls
 
                 builder.AppendLine("#EXTINF:" + length.ToString(UsCulture) + ",");
 
-                builder.AppendLine(string.Format("hlsdynamic/{0}/{1}.ts{2}",
+                builder.AppendLine(string.Format("hlsdynamic/{0}/{1}{2}{3}",
 
                     name,
                     index.ToString(UsCulture),
+                    GetSegmentFileExtension(isOutputVideo),
                     queryString));
 
                 seconds -= state.SegmentLength;
@@ -642,6 +777,28 @@ namespace MediaBrowser.Api.Playback.Hls
 
         protected override string GetAudioArguments(StreamState state)
         {
+            if (!state.IsOutputVideo)
+            {
+                var audioTranscodeParams = new List<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 +829,11 @@ namespace MediaBrowser.Api.Playback.Hls
 
         protected override string GetVideoArguments(StreamState state)
         {
+            if (!state.IsOutputVideo)
+            {
+                return string.Empty;
+            }
+
             var codec = state.OutputVideoCodec;
 
             var args = "-codec:v:0 " + codec;
@@ -684,30 +846,36 @@ namespace MediaBrowser.Api.Playback.Hls
             // See if we can save come cpu cycles by avoiding encoding
             if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
             {
-                return state.VideoStream != null && IsH264(state.VideoStream) ?
-                    args + " -bsf:v h264_mp4toannexb" :
-                    args;
+                args += state.VideoStream != null && IsH264(state.VideoStream)
+                    ? args + " -bsf:v h264_mp4toannexb"
+                    : args;
             }
+            else
+            {
+                var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})",
+                    state.SegmentLength.ToString(UsCulture));
 
-            var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})",
-                state.SegmentLength.ToString(UsCulture));
+                var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream;
 
-            var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream;
+                args += " " + GetVideoQualityParam(state, H264Encoder, true) + keyFrameArg;
 
-            args += " " + GetVideoQualityParam(state, H264Encoder, true) + keyFrameArg;
+                //args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
 
-            // Add resolution params, if specified
-            if (!hasGraphicalSubs)
-            {
-                args += GetOutputSizeParam(state, codec, false);
-            }
+                // Add resolution params, if specified
+                if (!hasGraphicalSubs)
+                {
+                    args += GetOutputSizeParam(state, codec, false);
+                }
 
-            // This is for internal graphical subs
-            if (hasGraphicalSubs)
-            {
-                args += GetGraphicalSubtitleParam(state, codec);
+                // This is for internal graphical subs
+                if (hasGraphicalSubs)
+                {
+                    args += GetGraphicalSubtitleParam(state, codec);
+                }
             }
 
+            args += " -flags +loop-global_header -sc_threshold 0";
+
             return args;
         }
 
@@ -715,43 +883,96 @@ namespace MediaBrowser.Api.Playback.Hls
         {
             var threads = GetNumberOfThreads(state, false);
 
-            var inputModifier = GetInputModifier(state);
+            var inputModifier = GetInputModifier(state, false);
 
             // If isEncoding is true we're actually starting ffmpeg
             var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0";
 
-            if (state.EnableGenericHlsSegmenter)
+            var toTimeParam = string.Empty;
+            if (EnableSplitTranscoding(state))
             {
-                var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d.ts";
+                var startTime = state.Request.StartTimeTicks ?? 0;
+                var durationSeconds = ApiEntryPoint.Instance.GetEncodingOptions().ThrottleThresholdInSeconds;
+
+                var endTime = startTime + TimeSpan.FromSeconds(durationSeconds).Ticks;
+                endTime = Math.Min(endTime, state.RunTimeTicks.Value);
+
+                if (endTime < state.RunTimeTicks.Value)
+                {
+                    //toTimeParam = " -to " + MediaEncoder.GetTimeParameter(endTime);
+                    toTimeParam = " -t " + MediaEncoder.GetTimeParameter(TimeSpan.FromSeconds(durationSeconds).Ticks);
+                }
+            }
 
-                return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -flags -global_header -sc_threshold 0 {5} -f segment -segment_time {6} -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
-                    inputModifier,
-                    GetInputArgument(state),
-                    threads,
-                    GetMapArgs(state),
-                    GetVideoArguments(state),
-                    GetAudioArguments(state),
-                    state.SegmentLength.ToString(UsCulture),
-                    startNumberParam,
-                    outputPath,
-                    outputTsArg
-                    ).Trim();
+            var timestampOffsetParam = string.Empty;
+            if (state.IsOutputVideo)
+            {
+                timestampOffsetParam = " -output_ts_offset " + MediaEncoder.GetTimeParameter(state.Request.StartTimeTicks ?? 0).ToString(CultureInfo.InvariantCulture);
             }
+            
+            var mapArgs = state.IsOutputVideo ? GetMapArgs(state) : string.Empty;
+
+            //var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state);
 
-            return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -flags -global_header -copyts -sc_threshold 0 {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"",
+            //return string.Format("{0} {11} {1}{10} -map_metadata -1 -threads {2} {3} {4} {5} -f segment -segment_time {6} -segment_format mpegts -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
+            //    inputModifier,
+            //    GetInputArgument(state),
+            //    threads,
+            //    mapArgs,
+            //    GetVideoArguments(state),
+            //    GetAudioArguments(state),
+            //    state.SegmentLength.ToString(UsCulture),
+            //    startNumberParam,
+            //    outputPath,
+            //    outputTsArg,
+            //            slowSeekParam,
+            //            toTimeParam
+            //    ).Trim();
+
+            return string.Format("{0}{11} {1} -map_metadata -1 -threads {2} {3} {4}{5} {6} -hls_time {7} -start_number {8} -hls_list_size {9} -y \"{10}\"",
                             inputModifier,
                             GetInputArgument(state),
                             threads,
-                            GetMapArgs(state),
+                            mapArgs,
                             GetVideoArguments(state),
+                            timestampOffsetParam,
                             GetAudioArguments(state),
                             state.SegmentLength.ToString(UsCulture),
                             startNumberParam,
                             state.HlsListSize.ToString(UsCulture),
-                            outputPath
+                            outputPath,
+                            toTimeParam
                             ).Trim();
         }
 
+        protected override bool EnableThrottling(StreamState state)
+        {
+            return !EnableSplitTranscoding(state);
+        }
+
+        private bool EnableSplitTranscoding(StreamState state)
+        {
+            if (string.Equals(Request.QueryString["EnableSplitTranscoding"], "false", StringComparison.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+
+            if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+
+            return state.RunTimeTicks.HasValue && state.IsOutputVideo;
+        }
+
+        protected override bool EnableStreamCopy
+        {
+            get
+            {
+                return false;
+            }
+        }
+
         /// <summary>
         /// Gets the segment file extension.
         /// </summary>
@@ -759,7 +980,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";
         }
     }
 }

+ 29 - 6
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>
@@ -29,12 +31,31 @@ namespace MediaBrowser.Api.Playback.Hls
         public string SegmentId { get; set; }
     }
 
+    /// <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);

+ 1 - 20
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);
         }

+ 1 - 1
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")]

+ 5 - 2
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

+ 1 - 1
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();
             }

+ 30 - 4
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);

+ 47 - 0
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
+	}
+}

+ 20 - 0
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
+	}
+}

+ 229 - 0
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;
+		}
+
+	}
+}

+ 12 - 0
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
+	}
+}

+ 19 - 0
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
+	}
+}

+ 12 - 0
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
+	}
+}

+ 101 - 0
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();
+			}
+		}
+	}
+}

+ 25 - 0
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
+	}
+}

+ 589 - 0
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;
+		}
+	}
+}

+ 212 - 0
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>");
+		}
+	}
+
+}

+ 44 - 0
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;
+		}
+	}
+}

+ 54 - 0
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; }
+
+	}
+}

+ 34 - 0
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;
+		}
+	}
+}

+ 52 - 0
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; }
+	}
+}

+ 53 - 0
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; }
+
+	}
+}

+ 71 - 0
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; }
+	}
+}

+ 0 - 9
MediaBrowser.Api/Reports/ReportFieldType.cs

@@ -1,9 +0,0 @@
-
-namespace MediaBrowser.Api.Reports
-{
-    public enum ReportFieldType
-    {
-        String,
-        Boolean
-    }
-}

+ 273 - 19
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; }
+	}
+
 }

+ 0 - 16
MediaBrowser.Api/Reports/ReportResult.cs

@@ -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>();
-        }
-    }
-}

+ 1141 - 42
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;
+		}
+
+	}
 }

+ 214 - 0
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);
+			}
+		}
+	}
+}

+ 37 - 0
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);
+		}
+	}
+}

+ 29 - 0
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; }
+
+	}
+}

+ 28 - 0
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; }	
+	}
+}

+ 2 - 1
MediaBrowser.Controller/LiveTv/ILiveTvManager.cs

@@ -158,9 +158,10 @@ namespace MediaBrowser.Controller.LiveTv
         /// Gets the channel stream.
         /// </summary>
         /// <param name="id">The identifier.</param>
+        /// <param name="mediaSourceId">The media source identifier.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{StreamResponseInfo}.</returns>
-        Task<MediaSourceInfo> GetChannelStream(string id, CancellationToken cancellationToken);
+        Task<MediaSourceInfo> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken);
         
         /// <summary>
         /// Gets the program.

+ 22 - 1
MediaBrowser.Controller/Providers/ItemInfo.cs

@@ -1,9 +1,30 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+
 namespace MediaBrowser.Controller.Providers
 {
     public class ItemInfo
     {
-        public string Path { get; set; }
+        public ItemInfo()
+        {
+        }
+
+        public ItemInfo(IHasMetadata item)
+        {
+            Path = item.Path;
+            ContainingFolderPath = item.ContainingFolderPath;
+            IsInMixedFolder = item.IsInMixedFolder;
 
+            var video = item as Video;
+            if (video != null)
+            {
+                VideoType = video.VideoType;
+            }
+        }
+
+        public string Path { get; set; }
+        public string ContainingFolderPath { get; set; }
+        public VideoType VideoType { get; set; }
         public bool IsInMixedFolder { get; set; }
     }
 }

+ 14 - 4
MediaBrowser.Dlna/PlayTo/Device.cs

@@ -635,15 +635,25 @@ namespace MediaBrowser.Dlna.PlayTo
             }
 
             XElement uPnpResponse;
-
+            
+            // Handle different variations sent back by devices
             try
             {
                 uPnpResponse = XElement.Parse(trackString);
             }
-            catch (Exception ex)
+            catch (Exception)
             {
-                _logger.ErrorException("Unable to parse xml {0}", ex, trackString);
-                return new Tuple<bool, uBaseObject>(true, null);
+                // first try to add a root node with a dlna namesapce
+                try
+                {
+                    uPnpResponse = XElement.Parse("<data xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" + trackString + "</data>");
+                    uPnpResponse = uPnpResponse.Descendants().First();
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Unable to parse xml {0}", ex, trackString);
+                    return new Tuple<bool, uBaseObject>(true, null);
+                }
             }
 
             var e = uPnpResponse.Element(uPnpNamespaces.items);

+ 35 - 17
MediaBrowser.Dlna/PlayTo/PlayToController.cs

@@ -265,7 +265,7 @@ namespace MediaBrowser.Dlna.PlayTo
         {
             var ticks = _device.Position.Ticks;
 
-            if (!info.IsDirectStream)
+            if (!EnableClientSideSeek(info))
             {
                 ticks += info.StartPositionTicks;
             }
@@ -376,23 +376,28 @@ namespace MediaBrowser.Dlna.PlayTo
             {
                 var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
 
-                if (info.Item != null && !info.IsDirectStream)
+                if (info.Item != null && !EnableClientSideSeek(info))
                 {
                     var user = _session.UserId.HasValue ? _userManager.GetUserById(_session.UserId.Value) : null;
                     var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
 
                     await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl).ConfigureAwait(false);
-
-                    if (newItem.StreamInfo.IsDirectStream)
-                    {
-                        await _device.Seek(TimeSpan.FromTicks(newPosition)).ConfigureAwait(false);
-                    }
                     return;
                 }
-                await _device.Seek(TimeSpan.FromTicks(newPosition)).ConfigureAwait(false);
+                await SeekAfterTransportChange(newPosition).ConfigureAwait(false);
             }
         }
 
+        private bool EnableClientSideSeek(StreamParams info)
+        {
+            return info.IsDirectStream;
+        }
+
+        private bool EnableClientSideSeek(StreamInfo info)
+        {
+            return info.IsDirectStream;
+        }
+
         public Task SendUserDataChangeInfo(UserDataChangeInfo info, CancellationToken cancellationToken)
         {
             return Task.FromResult(true);
@@ -607,8 +612,10 @@ namespace MediaBrowser.Dlna.PlayTo
             await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl);
 
             var streamInfo = currentitem.StreamInfo;
-            if (streamInfo.StartPositionTicks > 0 && streamInfo.IsDirectStream)
-                await _device.Seek(TimeSpan.FromTicks(streamInfo.StartPositionTicks));
+            if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
+            {
+                await SeekAfterTransportChange(streamInfo.StartPositionTicks).ConfigureAwait(false);
+            }
         }
 
         #endregion
@@ -742,9 +749,9 @@ namespace MediaBrowser.Dlna.PlayTo
 
                     await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl).ConfigureAwait(false);
 
-                    if (newItem.StreamInfo.IsDirectStream)
+                    if (EnableClientSideSeek(newItem.StreamInfo))
                     {
-                        await _device.Seek(TimeSpan.FromTicks(newPosition)).ConfigureAwait(false);
+                        await SeekAfterTransportChange(newPosition).ConfigureAwait(false);
                     }
                 }
             }
@@ -768,17 +775,28 @@ namespace MediaBrowser.Dlna.PlayTo
 
                     await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl).ConfigureAwait(false);
 
-                    if (newItem.StreamInfo.IsDirectStream && newPosition > 0)
+                    if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
                     {
-                        // This is rather arbitrary, but give the player time to start playing
-                        await Task.Delay(2000).ConfigureAwait(false);
-
-                        await _device.Seek(TimeSpan.FromTicks(newPosition)).ConfigureAwait(false);
+                        await SeekAfterTransportChange(newPosition).ConfigureAwait(false);
                     }
                 }
             }
         }
 
+        private async Task SeekAfterTransportChange(long positionTicks)
+        {
+            const int maxWait = 15000000;
+            const int interval = 500;
+            var currentWait = 0;
+            while (_device.TransportState != TRANSPORTSTATE.PLAYING && currentWait < maxWait)
+            {
+                await Task.Delay(interval).ConfigureAwait(false);
+                currentWait += interval;
+            }
+
+            await _device.Seek(TimeSpan.FromTicks(positionTicks)).ConfigureAwait(false);
+        }
+
         private class StreamParams
         {
             public string ItemId { get; set; }

+ 2 - 2
MediaBrowser.Dlna/Profiles/DefaultProfile.cs

@@ -31,8 +31,8 @@ namespace MediaBrowser.Dlna.Profiles
             MaxIconWidth = 48;
             MaxIconHeight = 48;
 
-            MaxStreamingBitrate = 8000000;
-            MaxStaticBitrate = 8000000;
+            MaxStreamingBitrate = 10000000;
+            MaxStaticBitrate = 10000000;
             MusicStreamingTranscodingBitrate = 128000;
             MusicSyncBitrate = 128000;
 

+ 1 - 1
MediaBrowser.Dlna/Profiles/WdtvLiveProfile.cs

@@ -15,7 +15,7 @@ namespace MediaBrowser.Dlna.Profiles
 
             Identification = new DeviceIdentification
             {
-                ModelName = "WD TV HD Live",
+                ModelName = "WD TV",
 
                 Headers = new []
                 {

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/BubbleUPnp.xml

@@ -22,8 +22,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -34,7 +34,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes />
   <DirectPlayProfiles>
     <DirectPlayProfile container="avi,mpeg,mkv,ts,mp4,mov,m4v,asf,webm,ogg,ogv,iso" type="Video" />

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/Default.xml

@@ -16,8 +16,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -28,7 +28,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes />
   <DirectPlayProfiles>
     <DirectPlayProfile container="mp3,wma" type="Audio" />

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/Denon AVR.xml

@@ -21,8 +21,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -33,7 +33,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes />
   <DirectPlayProfiles>
     <DirectPlayProfile container="mp3,flac,m4a,wma" type="Audio" />

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/DirecTV HD-DVR.xml

@@ -22,8 +22,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -34,7 +34,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes />
   <DirectPlayProfiles>
     <DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg2video" type="Video" />

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/Dish Hopper-Joey.xml

@@ -23,8 +23,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -35,7 +35,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes />
   <DirectPlayProfiles>
     <DirectPlayProfile container="mp4,mkv,mpeg,ts" audioCodec="mp3,ac3,aac,he-aac,pcm" videoCodec="h264,mpeg2video" type="Video" />

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 23
MediaBrowser.Dlna/Profiles/Xml/Generic Device.xml


+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/LG Smart TV.xml

@@ -22,8 +22,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -34,7 +34,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes />
   <DirectPlayProfiles>
     <DirectPlayProfile container="ts" audioCodec="aac,ac3,mp3" videoCodec="h264" type="Video" />

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/Linksys DMA2100.xml

@@ -20,8 +20,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -32,7 +32,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes />
   <DirectPlayProfiles>
     <DirectPlayProfile container="mp3,flac,m4a,wma" type="Audio" />

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/MediaMonkey.xml

@@ -22,8 +22,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -34,7 +34,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes />
   <DirectPlayProfiles>
     <DirectPlayProfile container="mp3" audioCodec="mp2,mp3" type="Audio" />

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml

@@ -23,8 +23,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -35,7 +35,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes>
     <XmlAttribute name="xmlns:pv" value="http://www.pv.com/pvns/" />
   </XmlRootAttributes>

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/Popcorn Hour.xml

@@ -16,8 +16,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -28,7 +28,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes />
   <DirectPlayProfiles>
     <DirectPlayProfile container="mp4,mov" audioCodec="aac" videoCodec="h264,mpeg4" type="Video" />

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml

@@ -22,8 +22,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -34,7 +34,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes>
     <XmlAttribute name="xmlns:sec" value="http://www.sec.co.kr/" />
   </XmlRootAttributes>

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml

@@ -22,8 +22,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -34,7 +34,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes>
     <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
   </XmlRootAttributes>

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player.xml

@@ -24,8 +24,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -36,7 +36,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes>
     <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
   </XmlRootAttributes>

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2010).xml

@@ -23,8 +23,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -36,7 +36,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes>
     <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
   </XmlRootAttributes>

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2011).xml

@@ -23,8 +23,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -36,7 +36,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes>
     <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
   </XmlRootAttributes>

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2012).xml

@@ -23,8 +23,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -36,7 +36,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes>
     <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
   </XmlRootAttributes>

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2013).xml

@@ -23,8 +23,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -36,7 +36,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes>
     <XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
   </XmlRootAttributes>

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/Sony PlayStation 3.xml

@@ -23,8 +23,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -36,7 +36,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes />
   <DirectPlayProfiles>
     <DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/Vlc.xml

@@ -22,8 +22,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -34,7 +34,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes />
   <DirectPlayProfiles>
     <DirectPlayProfile container="avi,mpeg,mkv,ts,mp4,mov,m4v,asf,webm,ogg,ogv,iso" type="Video" />

+ 3 - 4
MediaBrowser.Dlna/Profiles/Xml/WDTV Live.xml

@@ -2,7 +2,7 @@
 <Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <Name>WDTV Live</Name>
   <Identification>
-    <ModelName>WD TV HD Live</ModelName>
+    <ModelName>WD TV</ModelName>
     <Headers>
       <HttpHeaderInfo name="User-Agent" value="alphanetworks" match="Substring" />
       <HttpHeaderInfo name="User-Agent" value="ALPHA Networks" match="Substring" />
@@ -23,8 +23,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -35,7 +35,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>true</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes />
   <DirectPlayProfiles>
     <DirectPlayProfile container="avi" audioCodec="ac3,dca,mp2,mp3,pcm" videoCodec="mpeg1video,mpeg2video,mpeg4,h264,vc1" type="Video" />

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/Xbox 360.xml

@@ -23,8 +23,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -35,7 +35,6 @@
   <EnableMSMediaReceiverRegistrar>true</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>false</EnableDlnaProtocol>
-  <EnableUrlBase>true</EnableUrlBase>
   <XmlRootAttributes />
   <DirectPlayProfiles>
     <DirectPlayProfile container="avi" audioCodec="ac3,mp3" videoCodec="mpeg4" type="Video" />

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/Xbox One.xml

@@ -23,8 +23,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -35,7 +35,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes />
   <DirectPlayProfiles>
     <DirectPlayProfile container="ts" audioCodec="ac3" videoCodec="h264" type="Video" />

+ 2 - 3
MediaBrowser.Dlna/Profiles/Xml/foobar2000.xml

@@ -22,8 +22,8 @@
   <MaxAlbumArtHeight>480</MaxAlbumArtHeight>
   <MaxIconWidth>48</MaxIconWidth>
   <MaxIconHeight>48</MaxIconHeight>
-  <MaxStreamingBitrate>8000000</MaxStreamingBitrate>
-  <MaxStaticBitrate>8000000</MaxStaticBitrate>
+  <MaxStreamingBitrate>10000000</MaxStreamingBitrate>
+  <MaxStaticBitrate>10000000</MaxStaticBitrate>
   <MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
   <MusicSyncBitrate>128000</MusicSyncBitrate>
   <XDlnaDoc>DMS-1.50</XDlnaDoc>
@@ -34,7 +34,6 @@
   <EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <EnableDlnaProtocol>true</EnableDlnaProtocol>
-  <EnableUrlBase>false</EnableUrlBase>
   <XmlRootAttributes />
   <DirectPlayProfiles>
     <DirectPlayProfile container="mp3" audioCodec="mp2,mp3" type="Audio" />

+ 1 - 1
MediaBrowser.LocalMetadata/BaseXmlProvider.cs

@@ -58,7 +58,7 @@ namespace MediaBrowser.LocalMetadata
 
         public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date)
         {
-            var file = GetXmlFile(new ItemInfo { IsInMixedFolder = item.IsInMixedFolder, Path = item.Path }, directoryService);
+            var file = GetXmlFile(new ItemInfo(item), directoryService);
 
             if (file == null)
             {

+ 3 - 25
MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs

@@ -2,13 +2,10 @@
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.MediaEncoding.Subtitles;
 using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
@@ -39,7 +36,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
         protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
 
-        public BaseEncoder(MediaEncoder mediaEncoder,
+        protected BaseEncoder(MediaEncoder mediaEncoder,
             ILogger logger,
             IServerConfigurationManager configurationManager,
             IFileSystem fileSystem,
@@ -64,7 +61,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             IProgress<double> progress,
             CancellationToken cancellationToken)
         {
-            var encodingJob = await new EncodingJobFactory(Logger, LibraryManager, MediaSourceManager)
+            var encodingJob = await new EncodingJobFactory(Logger, LibraryManager, MediaSourceManager, ConfigurationManager)
                 .CreateJob(options, IsVideoEncoder, progress, cancellationToken).ConfigureAwait(false);
 
             encodingJob.OutputFilePath = GetOutputFilePath(encodingJob);
@@ -305,25 +302,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return job.Options.CpuCoreLimit ?? 0;
         }
 
-        protected EncodingQuality GetQualitySetting()
-        {
-            var quality = GetEncodingOptions().EncodingQuality;
-
-            if (quality == EncodingQuality.Auto)
-            {
-                var cpuCount = Environment.ProcessorCount;
-
-                if (cpuCount >= 4)
-                {
-                    //return EncodingQuality.HighQuality;
-                }
-
-                return EncodingQuality.HighSpeed;
-            }
-
-            return quality;
-        }
-
         protected string GetInputModifier(EncodingJob job, bool genPts = true)
         {
             var inputModifier = string.Empty;
@@ -543,7 +521,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             var isVc1 = state.VideoStream != null &&
                 string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
 
-            var qualitySetting = GetQualitySetting();
+            var qualitySetting = state.Quality;
 
             if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
             {

+ 2 - 1
MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs

@@ -1,5 +1,6 @@
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Dto;
@@ -24,7 +25,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         public Stream LogFileStream { get; set; }
         public IProgress<double> Progress { get; set; }
         public TaskCompletionSource<bool> TaskCompletionSource;
-
+        public EncodingQuality Quality { get; set; }
         public EncodingJobOptions Options { get; set; }
         public string InputContainer { get; set; }
         public MediaSourceInfo MediaSource { get; set; }

+ 33 - 4
MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs

@@ -1,8 +1,7 @@
-using System.IO;
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
@@ -22,14 +21,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
         private readonly ILogger _logger;
         private readonly ILibraryManager _libraryManager;
         private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly IConfigurationManager _config;
 
         protected static readonly CultureInfo UsCulture = new CultureInfo("en-US");
         
-        public EncodingJobFactory(ILogger logger, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager)
+        public EncodingJobFactory(ILogger logger, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager, IConfigurationManager config)
         {
             _logger = logger;
             _libraryManager = libraryManager;
             _mediaSourceManager = mediaSourceManager;
+            _config = config;
         }
 
         public async Task<EncodingJob> CreateJob(EncodingJobOptions options, bool isVideoRequest, IProgress<double> progress, CancellationToken cancellationToken)
@@ -95,6 +96,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             TryStreamCopy(state, request);
 
+            state.Quality = options.Context == EncodingContext.Static ? 
+                EncodingQuality.MaxQuality :
+                GetQualitySetting();
+
             return state;
         }
 
@@ -194,6 +199,30 @@ namespace MediaBrowser.MediaEncoding.Encoder
             state.MediaSource = mediaSource;
         }
 
+        protected EncodingQuality GetQualitySetting()
+        {
+            var quality = GetEncodingOptions().EncodingQuality;
+
+            if (quality == EncodingQuality.Auto)
+            {
+                var cpuCount = Environment.ProcessorCount;
+
+                if (cpuCount >= 4)
+                {
+                    //return EncodingQuality.HighQuality;
+                }
+
+                return EncodingQuality.HighSpeed;
+            }
+
+            return quality;
+        }
+
+        protected EncodingOptions GetEncodingOptions()
+        {
+            return _config.GetConfiguration<EncodingOptions>("encoding");
+        }
+
         /// <summary>
         /// Infers the video codec.
         /// </summary>

+ 15 - 6
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -199,7 +199,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             await _ffProbeResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
 
-            using (var processWrapper = new ProcessWrapper(process, this))
+            using (var processWrapper = new ProcessWrapper(process, this, _logger))
             {
                 try
                 {
@@ -308,7 +308,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
 
-            using (var processWrapper = new ProcessWrapper(process, this))
+            using (var processWrapper = new ProcessWrapper(process, this, _logger))
             {
                 StartProcess(processWrapper);
 
@@ -492,7 +492,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
 
-            using (var processWrapper = new ProcessWrapper(process, this))
+            using (var processWrapper = new ProcessWrapper(process, this, _logger))
             {
                 bool ranToCompletion;
 
@@ -607,7 +607,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             bool ranToCompletion = false;
 
-            using (var processWrapper = new ProcessWrapper(process, this))
+            using (var processWrapper = new ProcessWrapper(process, this, _logger))
             {
                 try
                 {
@@ -792,11 +792,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
             public bool HasExited;
             public int? ExitCode;
             private readonly MediaEncoder _mediaEncoder;
+            private readonly ILogger _logger;
 
-            public ProcessWrapper(Process process, MediaEncoder mediaEncoder)
+            public ProcessWrapper(Process process, MediaEncoder mediaEncoder, ILogger logger)
             {
                 Process = process;
                 this._mediaEncoder = mediaEncoder;
+                _logger = logger;
                 Process.Exited += Process_Exited;
             }
 
@@ -806,7 +808,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
                 HasExited = true;
 
-                ExitCode = process.ExitCode;
+                try
+                {
+                    ExitCode = process.ExitCode;
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error determing process exit code", ex);
+                }
 
                 lock (_mediaEncoder._runningProcesses)
                 {

+ 4 - 39
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -129,7 +129,7 @@ namespace MediaBrowser.MediaEncoding.Probing
             }
             else if (string.Equals(streamInfo.codec_type, "video", StringComparison.OrdinalIgnoreCase))
             {
-                stream.Type = isAudio
+                stream.Type = isAudio || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
                     ? MediaStreamType.EmbeddedImage
                     : MediaStreamType.Video;
 
@@ -146,44 +146,8 @@ namespace MediaBrowser.MediaEncoding.Probing
                 //    string.Equals(stream.AspectRatio, "2.35:1", StringComparison.OrdinalIgnoreCase) ||
                 //    string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase);
 
-                if (string.Equals(streamInfo.sample_aspect_ratio, "1:1", StringComparison.OrdinalIgnoreCase))
-                {
-                    stream.IsAnamorphic = false;
-                }
-                else if (!((string.IsNullOrWhiteSpace(streamInfo.sample_aspect_ratio) || string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))))
-                {
-                    stream.IsAnamorphic = true;
-                }
-                else if (string.IsNullOrWhiteSpace(streamInfo.display_aspect_ratio) || string.Equals(streamInfo.display_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
-                {
-                    stream.IsAnamorphic = false;
-                }
-                else
-                {
-                    var ratioParts = streamInfo.display_aspect_ratio.Split(':');
-                    if (ratioParts.Length != 2)
-                    {
-                        stream.IsAnamorphic = false;
-                    }
-                    else
-                    {
-                        int ratio0;
-                        int ratio1;
-                        if (!Int32.TryParse(ratioParts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out ratio0))
-                        {
-                            stream.IsAnamorphic = false;
-                        }
-                        else if (!Int32.TryParse(ratioParts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out ratio1))
-                        {
-                            stream.IsAnamorphic = false;
-                        }
-                        else
-                        {
-                            stream.IsAnamorphic = ((streamInfo.width * ratio1) != (stream.Height * ratio0));
-                        }
-                    }
-                }
-            
+                // http://stackoverflow.com/questions/17353387/how-to-detect-anamorphic-video-with-ffprobe
+                stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase);
             }
             else
             {
@@ -519,6 +483,7 @@ namespace MediaBrowser.MediaEncoding.Probing
             FetchStudios(audio, tags, "organization");
             FetchStudios(audio, tags, "ensemble");
             FetchStudios(audio, tags, "publisher");
+            FetchStudios(audio, tags, "label");
 
             // These support mulitple values, but for now we only store the first.
             audio.SetProviderId(MetadataProviders.MusicBrainzAlbumArtist, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Artist Id")));

+ 2 - 2
MediaBrowser.Model/Configuration/EncodingOptions.cs

@@ -9,7 +9,7 @@ namespace MediaBrowser.Model.Configuration
         public string H264Encoder { get; set; }
         public bool EnableDebugLogging { get; set; }
         public bool EnableThrottling { get; set; }
-        public int ThrottleThresholdSeconds { get; set; }
+        public int ThrottleThresholdInSeconds { get; set; }
 
         public EncodingOptions()
         {
@@ -17,7 +17,7 @@ namespace MediaBrowser.Model.Configuration
             DownMixAudioBoost = 2;
             EncodingQuality = EncodingQuality.Auto;
             EnableThrottling = true;
-            ThrottleThresholdSeconds = 110;
+            ThrottleThresholdInSeconds = 120;
         }
     }
 }

+ 11 - 3
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -277,7 +277,7 @@ namespace MediaBrowser.Model.Configuration
                     {
                         new ImageOption
                         {
-                            Limit = 3,
+                            Limit = 2,
                             MinWidth = 1280,
                             Type = ImageType.Backdrop
                         },
@@ -304,7 +304,7 @@ namespace MediaBrowser.Model.Configuration
 
                         new ImageOption
                         {
-                            Limit = 1,
+                            Limit = 0,
                             Type = ImageType.Banner
                         },
 
@@ -374,7 +374,7 @@ namespace MediaBrowser.Model.Configuration
                     {
                         new ImageOption
                         {
-                            Limit = 1,
+                            Limit = 0,
                             MinWidth = 1280,
                             Type = ImageType.Backdrop
                         },
@@ -414,6 +414,14 @@ namespace MediaBrowser.Model.Configuration
                         {
                             Limit = 0,
                             Type = ImageType.Art
+                        },
+
+                        // Don't download this by default
+                        // Generally not used
+                        new ImageOption
+                        {
+                            Limit = 0,
+                            Type = ImageType.Logo
                         }
                     }
                 },

+ 1 - 1
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -485,7 +485,7 @@ namespace MediaBrowser.Model.Dlna
 
             if (targetAudioChannels.HasValue)
             {
-                if (targetAudioChannels.Value >= 5 && (maxTotalBitrate ?? 0) >= 1500000)
+                if (targetAudioChannels.Value >= 5 && (maxTotalBitrate ?? 0) >= 2000000)
                 {
                     defaultBitrate = 320000;
                 }

+ 5 - 0
MediaBrowser.Model/Dlna/StreamInfo.cs

@@ -158,6 +158,11 @@ namespace MediaBrowser.Model.Dlna
 
             if (MediaType == DlnaProfileType.Audio)
             {
+                if (StringHelper.EqualsIgnoreCase(SubProtocol, "hls"))
+                {
+                    return string.Format("{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
+                }
+
                 return string.Format("{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
             }
 

+ 6 - 0
MediaBrowser.Model/Updates/PackageInfo.cs

@@ -153,6 +153,12 @@ namespace MediaBrowser.Model.Updates
         /// <value>The versions.</value>
         public List<PackageVersionInfo> versions { get; set; }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether [enable in application store].
+        /// </summary>
+        /// <value><c>true</c> if [enable in application store]; otherwise, <c>false</c>.</value>
+        public bool enableInAppStore { get; set; }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="PackageInfo"/> class.
         /// </summary>

+ 1 - 1
MediaBrowser.Providers/Manager/MetadataService.cs

@@ -378,7 +378,7 @@ namespace MediaBrowser.Providers.Manager
                 var providerName = provider.GetType().Name;
                 Logger.Debug("Running {0} for {1}", providerName, logName);
 
-                var itemInfo = new ItemInfo { Path = item.Path, IsInMixedFolder = item.IsInMixedFolder };
+                var itemInfo = new ItemInfo(item);
 
                 try
                 {

+ 1 - 1
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs

@@ -130,7 +130,7 @@ namespace MediaBrowser.Providers.MediaInfo
             return ItemUpdateType.MetadataImport;
         }
 
-        private const string SchemaVersion = "4";
+        private const string SchemaVersion = "5";
 
         private async Task<Model.MediaInfo.MediaInfo> GetMediaInfo(Video item,
             IIsoMount isoMount,

+ 1 - 1
MediaBrowser.Providers/Music/FanArtAlbumProvider.cs

@@ -7,6 +7,7 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.TV;
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -16,7 +17,6 @@ using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Xml;
-using MediaBrowser.Providers.TV;
 
 namespace MediaBrowser.Providers.Music
 {

+ 10 - 1
MediaBrowser.Server.Implementations/Devices/DeviceManager.cs

@@ -1,6 +1,7 @@
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Events;
 using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Devices;
@@ -26,6 +27,7 @@ namespace MediaBrowser.Server.Implementations.Devices
         private readonly ILibraryMonitor _libraryMonitor;
         private readonly IConfigurationManager _config;
         private readonly ILogger _logger;
+        private readonly INetworkManager _network;
 
         public event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded;
 
@@ -34,7 +36,7 @@ namespace MediaBrowser.Server.Implementations.Devices
         /// </summary>
         public event EventHandler<GenericEventArgs<DeviceInfo>> DeviceOptionsUpdated;
 
-        public DeviceManager(IDeviceRepository repo, IUserManager userManager, IFileSystem fileSystem, ILibraryMonitor libraryMonitor, IConfigurationManager config, ILogger logger)
+        public DeviceManager(IDeviceRepository repo, IUserManager userManager, IFileSystem fileSystem, ILibraryMonitor libraryMonitor, IConfigurationManager config, ILogger logger, INetworkManager network)
         {
             _repo = repo;
             _userManager = userManager;
@@ -42,6 +44,7 @@ namespace MediaBrowser.Server.Implementations.Devices
             _libraryMonitor = libraryMonitor;
             _config = config;
             _logger = logger;
+            _network = network;
         }
 
         public async Task<DeviceInfo> RegisterDevice(string reportedId, string name, string appName, string appVersion, string usedByUserId)
@@ -233,6 +236,12 @@ namespace MediaBrowser.Server.Implementations.Devices
             }
 
             var user = _userManager.GetUserById(userId);
+
+            if (user == null)
+            {
+                throw new ArgumentException("user not found");
+            }
+
             if (!CanAccessDevice(user.Policy, deviceId))
             {
                 var capabilities = GetCapabilities(deviceId);

+ 20 - 9
MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs

@@ -45,19 +45,15 @@ namespace MediaBrowser.Server.Implementations.HttpServer
                 }
             }
 
-            if (dto is CompressedResult)
-            {
-                // Per Google PageSpeed
-                // This instructs the proxies to cache two versions of the resource: one compressed, and one uncompressed. 
-                // The correct version of the resource is delivered based on the client request header. 
-                // This is a good choice for applications that are singly homed and depend on public proxies for user locality.                        
-                res.AddHeader("Vary", "Accept-Encoding");
-            }
+            var vary = "Accept-Encoding";
 
             var hasOptions = dto as IHasOptions;
+            var sharpResponse = res as WebSocketSharpResponse;
 
             if (hasOptions != null)
             {
+                //hasOptions.Options["Server"] = "Mono-HTTPAPI/1.1";
+
                 // Content length has to be explicitly set on on HttpListenerResponse or it won't be happy
                 string contentLength;
 
@@ -79,14 +75,29 @@ namespace MediaBrowser.Server.Implementations.HttpServer
                             return;
                         }
 
-                        var sharpResponse = res as WebSocketSharpResponse;
                         if (sharpResponse != null)
                         {
                             sharpResponse.SendChunked = false;
                         }
                     }
                 }
+
+                string hasOptionsVary;
+                if (hasOptions.Options.TryGetValue("Vary", out hasOptionsVary))
+                {
+                    vary = hasOptionsVary;
+                }
+
+                hasOptions.Options["Vary"] = vary;
             }
+
+            //res.KeepAlive = false;
+
+            // Per Google PageSpeed
+            // This instructs the proxies to cache two versions of the resource: one compressed, and one uncompressed. 
+            // The correct version of the resource is delivered based on the client request header. 
+            // This is a good choice for applications that are singly homed and depend on public proxies for user locality.                        
+            res.AddHeader("Vary", vary);
         }
 
         /// <summary>

+ 2 - 2
MediaBrowser.Server.Implementations/HttpServer/Security/SessionContext.cs

@@ -34,8 +34,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security
             //    }
             //}
 
-            var session = _sessionManager.GetSession(authorization.DeviceId, authorization.Client, authorization.Version);
-            return Task.FromResult(session);
+            var user = string.IsNullOrWhiteSpace(authorization.UserId) ? null : _userManager.GetUserById(authorization.UserId);
+            return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.RemoteIp, user);
         }
 
         private AuthenticationInfo GetTokenInfo(IServiceRequest request)

+ 3 - 3
MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs

@@ -82,9 +82,9 @@ namespace MediaBrowser.Server.Implementations.IO
             }
 
             // This is an arbitraty amount of time, but delay it because file system writes often trigger events after RemoveTempIgnore has been called. 
-            // Seeing long delays in some situations, especially over the network.
-            // Seeing delays up to 40 seconds, but not going to ignore changes for that long.
-            await Task.Delay(5000).ConfigureAwait(false);
+            // Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds
+            // But if we make this delay too high, we risk missing legitimate changes
+            await Task.Delay(10000).ConfigureAwait(false);
 
             string val;
             _tempIgnoredPaths.TryRemove(path, out val);

+ 1 - 0
MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs

@@ -32,6 +32,7 @@ namespace MediaBrowser.Server.Implementations.Library
                 ".wd_tv",
 
                 // Synology
+                "@eaDir",
                 "eaDir"
 
         };

+ 26 - 6
MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -17,6 +17,7 @@ using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Serialization;
 using System;
@@ -330,12 +331,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
         public async Task<MediaSourceInfo> GetRecordingStream(string id, CancellationToken cancellationToken)
         {
-            return await GetLiveStream(id, false, cancellationToken).ConfigureAwait(false);
+            return await GetLiveStream(id, null, false, cancellationToken).ConfigureAwait(false);
         }
 
-        public async Task<MediaSourceInfo> GetChannelStream(string id, CancellationToken cancellationToken)
+        public async Task<MediaSourceInfo> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken)
         {
-            return await GetLiveStream(id, true, cancellationToken).ConfigureAwait(false);
+            return await GetLiveStream(id, mediaSourceId, true, cancellationToken).ConfigureAwait(false);
         }
 
         public async Task<IEnumerable<MediaSourceInfo>> GetRecordingMediaSources(string id, CancellationToken cancellationToken)
@@ -351,7 +352,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             var item = GetInternalChannel(id);
             var service = GetService(item);
 
-            return await service.GetChannelStreamMediaSources(id, cancellationToken).ConfigureAwait(false);
+            var sources = await service.GetChannelStreamMediaSources(item.ExternalId, cancellationToken).ConfigureAwait(false);
+            var list = sources.ToList();
+
+            foreach (var source in list)
+            {
+                Normalize(source, item.ChannelType == ChannelType.TV);
+            }
+
+            return list;
         }
 
         private ILiveTvService GetService(ILiveTvItem item)
@@ -364,7 +373,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return _services.FirstOrDefault(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
         }
 
-        private async Task<MediaSourceInfo> GetLiveStream(string id, bool isChannel, CancellationToken cancellationToken)
+        private async Task<MediaSourceInfo> GetLiveStream(string id, string mediaSourceId, bool isChannel, CancellationToken cancellationToken)
         {
             await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
 
@@ -379,7 +388,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                     isVideo = channel.ChannelType == ChannelType.TV;
                     var service = GetService(channel);
                     _logger.Info("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId);
-                    info = await service.GetChannelStream(channel.ExternalId, null, cancellationToken).ConfigureAwait(false);
+                    info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false);
                     info.RequiresClosing = true;
 
                     if (info.RequiresClosing)
@@ -519,6 +528,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                     stream.Index = -1;
                 }
             }
+
+            // Set the total bitrate if not already supplied
+            if (!mediaSource.Bitrate.HasValue)
+            {
+                var total = mediaSource.MediaStreams.Select(i => i.BitRate ?? 0).Sum();
+
+                if (total > 0)
+                {
+                    mediaSource.Bitrate = total;
+                }
+            }
         }
 
         private async Task<LiveTvChannel> GetChannel(ChannelInfo channelInfo, string serviceName, CancellationToken cancellationToken)

+ 35 - 12
MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs

@@ -1,10 +1,12 @@
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Serialization;
 using System;
 using System.Collections.Generic;
@@ -21,13 +23,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         private readonly ILogger _logger;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaEncoder _mediaEncoder;
+        private readonly IServerApplicationHost _appHost;
 
-        public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IJsonSerializer jsonSerializer, ILogManager logManager, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
+        public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IJsonSerializer jsonSerializer, ILogManager logManager, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IServerApplicationHost appHost)
         {
             _liveTvManager = liveTvManager;
             _jsonSerializer = jsonSerializer;
             _mediaSourceManager = mediaSourceManager;
             _mediaEncoder = mediaEncoder;
+            _appHost = appHost;
             _logger = logManager.GetLogger(GetType().Name);
         }
 
@@ -74,6 +78,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             }
 
             var list = sources.ToList();
+            var serverUrl = _appHost.LocalApiUrl;
 
             foreach (var source in list)
             {
@@ -84,7 +89,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 var openKeys = new List<string>();
                 openKeys.Add(item.GetType().Name);
                 openKeys.Add(item.Id.ToString("N"));
+                openKeys.Add(source.Id ?? string.Empty);
                 source.OpenToken = string.Join("|", openKeys.ToArray());
+
+                // Dummy this up so that direct play checks can still run
+                if (string.IsNullOrEmpty(source.Path) && source.Protocol == MediaProtocol.Http)
+                {
+                    source.Path = serverUrl;
+                }
             }
 
             _logger.Debug("MediaSources: {0}", _jsonSerializer.SerializeToString(list));
@@ -95,13 +107,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         public async Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken)
         {
             MediaSourceInfo stream;
-            var isAudio = false;
+            const bool isAudio = false;
 
-            var keys = openToken.Split(new[] { '|' }, 2);
+            var keys = openToken.Split(new[] { '|' }, 3);
+            var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
 
             if (string.Equals(keys[0], typeof(LiveTvChannel).Name, StringComparison.OrdinalIgnoreCase))
             {
-                stream = await _liveTvManager.GetChannelStream(keys[1], cancellationToken).ConfigureAwait(false);
+                stream = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, cancellationToken).ConfigureAwait(false);
             }
             else
             {
@@ -162,30 +175,40 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 mediaSource.DefaultAudioStreamIndex = audioStream.Index;
             }
 
-            // Try to estimate this
-            if (!mediaSource.Bitrate.HasValue)
+            var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Video);
+            if (videoStream != null)
             {
-                var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Video);
-                if (videoStream != null)
+                if (!videoStream.BitRate.HasValue)
                 {
                     var width = videoStream.Width ?? 1920;
 
                     if (width >= 1900)
                     {
-                        mediaSource.Bitrate = 10000000;
+                        videoStream.BitRate = 8000000;
                     }
 
                     else if (width >= 1260)
                     {
-                        mediaSource.Bitrate = 6000000;
+                        videoStream.BitRate = 3000000;
                     }
 
                     else if (width >= 700)
                     {
-                        mediaSource.Bitrate = 4000000;
+                        videoStream.BitRate = 1000000;
                     }
                 }
             }
+
+            // Try to estimate this
+            if (!mediaSource.Bitrate.HasValue)
+            {
+                var total = mediaSource.MediaStreams.Select(i => i.BitRate ?? 0).Sum();
+
+                if (total > 0)
+                {
+                    mediaSource.Bitrate = total;
+                }
+            }
         }
 
         public Task CloseMediaSource(string liveStreamId, CancellationToken cancellationToken)

+ 6 - 2
MediaBrowser.Server.Implementations/Localization/JavaScript/ar.json

@@ -40,6 +40,7 @@
     "TitleLiveTV": "Live TV",
     "TitleSync": "Sync",
     "ButtonDonate": "Donate",
+    "LabelRecurringDonationCanBeCancelledHelp": "Recurring donations can be cancelled at any time from within your PayPal account.",
     "HeaderMyMedia": "My Media",
     "TitleNotifications": "Notifications",
     "ErrorLaunchingChromecast": "There was an error launching chromecast. Please ensure your device is connected to your wireless network.",
@@ -97,7 +98,7 @@
     "HeaderSupporterBenefit": "A supporter membership provides additional benefits such as access to sync, premium plugins, internet channel content, and more. {0}Learn more{1}.",
     "LabelSyncNoTargetsHelp": "It looks like you don't currently have any apps that support sync.",
     "HeaderWelcomeToProjectServerDashboard": "Welcome to the Emby Server Dashboard",
-    "HeaderWelcomeToProjectWebClient": "Welcome to the Emby Web Client",
+    "HeaderWelcomeToProjectWebClient": "Welcome to Emby",
     "ButtonTakeTheTour": "Take the tour",
     "HeaderWelcomeBack": "Welcome back!",
     "TitlePlugins": "Plugins",
@@ -762,5 +763,8 @@
     "ButtonSignInWithConnect": "Sign in with Emby Connect",
     "HeaderNewServer": "New Server",
     "MyDevice": "My Device",
-    "ButtonRemote": "Remote"
+    "ButtonRemote": "Remote",
+    "TabInfo": "Info",
+    "TabCast": "Cast",
+    "TabScenes": "Scenes"
 }

+ 6 - 2
MediaBrowser.Server.Implementations/Localization/JavaScript/be_BY.json → MediaBrowser.Server.Implementations/Localization/JavaScript/be-BY.json

@@ -40,6 +40,7 @@
     "TitleLiveTV": "Live TV",
     "TitleSync": "Sync",
     "ButtonDonate": "Donate",
+    "LabelRecurringDonationCanBeCancelledHelp": "Recurring donations can be cancelled at any time from within your PayPal account.",
     "HeaderMyMedia": "My Media",
     "TitleNotifications": "Notifications",
     "ErrorLaunchingChromecast": "There was an error launching chromecast. Please ensure your device is connected to your wireless network.",
@@ -97,7 +98,7 @@
     "HeaderSupporterBenefit": "A supporter membership provides additional benefits such as access to sync, premium plugins, internet channel content, and more. {0}Learn more{1}.",
     "LabelSyncNoTargetsHelp": "It looks like you don't currently have any apps that support sync.",
     "HeaderWelcomeToProjectServerDashboard": "Welcome to the Emby Server Dashboard",
-    "HeaderWelcomeToProjectWebClient": "Welcome to the Emby Web Client",
+    "HeaderWelcomeToProjectWebClient": "Welcome to Emby",
     "ButtonTakeTheTour": "Take the tour",
     "HeaderWelcomeBack": "Welcome back!",
     "TitlePlugins": "Plugins",
@@ -762,5 +763,8 @@
     "ButtonSignInWithConnect": "Sign in with Emby Connect",
     "HeaderNewServer": "New Server",
     "MyDevice": "My Device",
-    "ButtonRemote": "Remote"
+    "ButtonRemote": "Remote",
+    "TabInfo": "Info",
+    "TabCast": "Cast",
+    "TabScenes": "Scenes"
 }

+ 6 - 2
MediaBrowser.Server.Implementations/Localization/JavaScript/bg_BG.json → MediaBrowser.Server.Implementations/Localization/JavaScript/bg-BG.json

@@ -40,6 +40,7 @@
     "TitleLiveTV": "\u0422V \u043d\u0430 \u0436\u0438\u0432\u043e",
     "TitleSync": "Sync",
     "ButtonDonate": "Donate",
+    "LabelRecurringDonationCanBeCancelledHelp": "Recurring donations can be cancelled at any time from within your PayPal account.",
     "HeaderMyMedia": "My Media",
     "TitleNotifications": "\u0418\u0437\u0432\u0435\u0441\u0442\u0438\u044f",
     "ErrorLaunchingChromecast": "There was an error launching chromecast. Please ensure your device is connected to your wireless network.",
@@ -97,7 +98,7 @@
     "HeaderSupporterBenefit": "A supporter membership provides additional benefits such as access to sync, premium plugins, internet channel content, and more. {0}Learn more{1}.",
     "LabelSyncNoTargetsHelp": "It looks like you don't currently have any apps that support sync.",
     "HeaderWelcomeToProjectServerDashboard": "Welcome to the Emby Server Dashboard",
-    "HeaderWelcomeToProjectWebClient": "Welcome to the Emby Web Client",
+    "HeaderWelcomeToProjectWebClient": "Welcome to Emby",
     "ButtonTakeTheTour": "\u0420\u0430\u0437\u0433\u043b\u0435\u0434\u0430\u0439 \u043d\u0430\u043e\u043a\u043e\u043b\u043e",
     "HeaderWelcomeBack": "Welcome back!",
     "TitlePlugins": "Plugins",
@@ -762,5 +763,8 @@
     "ButtonSignInWithConnect": "Sign in with Emby Connect",
     "HeaderNewServer": "New Server",
     "MyDevice": "My Device",
-    "ButtonRemote": "Remote"
+    "ButtonRemote": "Remote",
+    "TabInfo": "Info",
+    "TabCast": "Cast",
+    "TabScenes": "Scenes"
 }

+ 6 - 2
MediaBrowser.Server.Implementations/Localization/JavaScript/ca.json

@@ -40,6 +40,7 @@
     "TitleLiveTV": "Live TV",
     "TitleSync": "Sync",
     "ButtonDonate": "Donate",
+    "LabelRecurringDonationCanBeCancelledHelp": "Recurring donations can be cancelled at any time from within your PayPal account.",
     "HeaderMyMedia": "My Media",
     "TitleNotifications": "Notifications",
     "ErrorLaunchingChromecast": "There was an error launching chromecast. Please ensure your device is connected to your wireless network.",
@@ -97,7 +98,7 @@
     "HeaderSupporterBenefit": "A supporter membership provides additional benefits such as access to sync, premium plugins, internet channel content, and more. {0}Learn more{1}.",
     "LabelSyncNoTargetsHelp": "It looks like you don't currently have any apps that support sync.",
     "HeaderWelcomeToProjectServerDashboard": "Welcome to the Emby Server Dashboard",
-    "HeaderWelcomeToProjectWebClient": "Welcome to the Emby Web Client",
+    "HeaderWelcomeToProjectWebClient": "Welcome to Emby",
     "ButtonTakeTheTour": "Take the tour",
     "HeaderWelcomeBack": "Welcome back!",
     "TitlePlugins": "Plugins",
@@ -762,5 +763,8 @@
     "ButtonSignInWithConnect": "Sign in with Emby Connect",
     "HeaderNewServer": "New Server",
     "MyDevice": "My Device",
-    "ButtonRemote": "Remote"
+    "ButtonRemote": "Remote",
+    "TabInfo": "Info",
+    "TabCast": "Cast",
+    "TabScenes": "Scenes"
 }

+ 6 - 2
MediaBrowser.Server.Implementations/Localization/JavaScript/cs.json

@@ -40,6 +40,7 @@
     "TitleLiveTV": "\u017div\u00e1 TV",
     "TitleSync": "Sync",
     "ButtonDonate": "Donate",
+    "LabelRecurringDonationCanBeCancelledHelp": "Recurring donations can be cancelled at any time from within your PayPal account.",
     "HeaderMyMedia": "My Media",
     "TitleNotifications": "Notifications",
     "ErrorLaunchingChromecast": "There was an error launching chromecast. Please ensure your device is connected to your wireless network.",
@@ -97,7 +98,7 @@
     "HeaderSupporterBenefit": "A supporter membership provides additional benefits such as access to sync, premium plugins, internet channel content, and more. {0}Learn more{1}.",
     "LabelSyncNoTargetsHelp": "It looks like you don't currently have any apps that support sync.",
     "HeaderWelcomeToProjectServerDashboard": "Welcome to the Emby Server Dashboard",
-    "HeaderWelcomeToProjectWebClient": "Welcome to the Emby Web Client",
+    "HeaderWelcomeToProjectWebClient": "Welcome to Emby",
     "ButtonTakeTheTour": "Take the tour",
     "HeaderWelcomeBack": "Welcome back!",
     "TitlePlugins": "Plugins",
@@ -762,5 +763,8 @@
     "ButtonSignInWithConnect": "Sign in with Emby Connect",
     "HeaderNewServer": "New Server",
     "MyDevice": "My Device",
-    "ButtonRemote": "Remote"
+    "ButtonRemote": "Remote",
+    "TabInfo": "Info",
+    "TabCast": "Cast",
+    "TabScenes": "Scenes"
 }

+ 648 - 644
MediaBrowser.Server.Implementations/Localization/JavaScript/da.json

@@ -4,174 +4,175 @@
     "Users": "Brugere",
     "Delete": "Slet",
     "Administrator": "Administrator",
-    "Password": "Kode",
-    "DeleteImage": "Slet Image",
-    "MessageThankYouForSupporting": "Thank you for supporting Emby.",
-    "MessagePleaseSupportProject": "Please support Emby.",
-    "DeleteImageConfirmation": "Er du sikker p\u00e5 du vil slette dette image?",
-    "FileReadCancelled": "L\u00e6sning af filen er annulleret",
-    "FileNotFound": "Filen blev ikke fundet",
-    "FileReadError": "Der opstod en fejl i fors\u00f8get p\u00e5 at l\u00e6se filen",
-    "DeleteUser": "Slet bruger",
+    "Password": "Adgangskode",
+    "DeleteImage": "Slet billede",
+    "MessageThankYouForSupporting": "Tak for at du st\u00f8tter Emby.",
+    "MessagePleaseSupportProject": "V\u00e6r venlig at st\u00f8tte Emby.",
+    "DeleteImageConfirmation": "Er du sikker p\u00e5 du vil slette dette billede?",
+    "FileReadCancelled": "L\u00e6sning af filen er annulleret.",
+    "FileNotFound": "Filen blev ikke fundet.",
+    "FileReadError": "Der opstod en fejl i fors\u00f8get p\u00e5 at l\u00e6se filen.",
+    "DeleteUser": "Slet bruger.",
     "DeleteUserConfirmation": "Er du sikker p\u00e5 du \u00f8nsker at slette denne bruger?",
-    "PasswordResetHeader": "Reset Password",
-    "PasswordResetComplete": "Koden er blevet nulstillet",
-    "PinCodeResetComplete": "The pin code has been reset.",
-    "PasswordResetConfirmation": "Er du sikker p\u00e5 at koden skal nulstilles",
-    "PinCodeResetConfirmation": "Are you sure you wish to reset the pin code?",
-    "HeaderPinCodeReset": "Reset Pin Code",
-    "PasswordSaved": "Koden er gemt",
-    "PasswordMatchError": "Kode og kode bekr\u00e6ftelse skal v\u00e6re ens.",
-    "OptionRelease": "Officiel Udgivelse",
+    "PasswordResetHeader": "Nulstil adgangskode",
+    "PasswordResetComplete": "Adgangskoden er blevet nulstillet.",
+    "PinCodeResetComplete": "Pinkoden er blevet nulstillet.",
+    "PasswordResetConfirmation": "Er du sikker p\u00e5 at adgangskoden skal nulstilles?",
+    "PinCodeResetConfirmation": "Er du sikker p\u00e5 at pinkoden skal nulstilles?",
+    "HeaderPinCodeReset": "Nulstil pinkode",
+    "PasswordSaved": "Adgangskoden er gemt.",
+    "PasswordMatchError": "Adgangskode og  bekr\u00e6ft adgangskode skal v\u00e6re ens.",
+    "OptionRelease": "Officiel udgivelse",
     "OptionBeta": "Beta",
     "OptionDev": "Dev (Ustabil)",
     "UninstallPluginHeader": "Afinstaller plugin",
-    "UninstallPluginConfirmation": "Er du sikker p\u00e5 du \u00f8nsker at afinstallere {0}?",
-    "NoPluginConfigurationMessage": "Der er igenting at konfigurere i dette plugin.",
-    "NoPluginsInstalledMessage": "Der er ikke installeret nogle plugins",
-    "BrowsePluginCatalogMessage": "Gennemse vores katalog med tilf\u00f8jelser for at se tilg\u00e6ngelige tilf\u00f8jelser.",
-    "MessageKeyEmailedTo": "Key emailed to {0}.",
-    "MessageKeysLinked": "Keys linked.",
-    "HeaderConfirmation": "Confirmation",
-    "MessageKeyUpdated": "Thank you. Your supporter key has been updated.",
-    "MessageKeyRemoved": "Thank you. Your supporter key has been removed.",
-    "HeaderSupportTheTeam": "Support the Emby Team",
-    "TextEnjoyBonusFeatures": "Enjoy Bonus Features",
-    "TitleLiveTV": "Direkte TV",
+    "UninstallPluginConfirmation": "Er du sikker p\u00e5 du vil afinstallere {0}?",
+    "NoPluginConfigurationMessage": "Der er ingenting at konfigurere i dette plugin.",
+    "NoPluginsInstalledMessage": "Der er ikke installeret nogle plugins.",
+    "BrowsePluginCatalogMessage": "Gennemse vores plugin-katalog for at se tilg\u00e6ngelige plugins.",
+    "MessageKeyEmailedTo": "N\u00f8gle sendt med e-mail til {0}.",
+    "MessageKeysLinked": "N\u00f8gler sammenknyttet.",
+    "HeaderConfirmation": "Bekr\u00e6ftelse",
+    "MessageKeyUpdated": "Tak. Din supporter n\u00f8gle er nu opdateret.",
+    "MessageKeyRemoved": "Tak. Din supporter n\u00f8gle er fjernet.",
+    "HeaderSupportTheTeam": "St\u00f8t Emby-holdet",
+    "TextEnjoyBonusFeatures": "F\u00e5 bonus funktioner",
+    "TitleLiveTV": "Live TV",
     "TitleSync": "Sync",
-    "ButtonDonate": "Donate",
-    "HeaderMyMedia": "My Media",
-    "TitleNotifications": "Notifications",
-    "ErrorLaunchingChromecast": "There was an error launching chromecast. Please ensure your device is connected to your wireless network.",
-    "MessageErrorLoadingSupporterInfo": "There was an error loading supporter information. Please try again later.",
-    "MessageLinkYourSupporterKey": "Link your supporter key with up to {0} Emby Connect members to enjoy free access to the following apps:",
-    "HeaderConfirmRemoveUser": "Remove User",
-    "MessageSwipeDownOnRemoteControl": "Welcome to remote control. Select the device to control by clicking the cast icon in the upper right corner. Swipe down anywhere on this screen to go back to where you came from.",
-    "MessageConfirmRemoveConnectSupporter": "Are you sure you wish to remove additional supporter benefits from this user?",
-    "ValueTimeLimitSingleHour": "Time limit: 1 hour",
-    "ValueTimeLimitMultiHour": "Time limit: {0} hours",
+    "ButtonDonate": "Don\u00e9r",
+    "LabelRecurringDonationCanBeCancelledHelp": "Tilbagevendende donationer kan afmeldes n\u00e5r som helst fra din PayPal konto.",
+    "HeaderMyMedia": "Mine medier",
+    "TitleNotifications": "Underretninger",
+    "ErrorLaunchingChromecast": "Der opstod en fejl ved start af cromecast. Tjek venligst at din enhed er forbundet til det tr\u00e5dl\u00f8se netv\u00e6rk.",
+    "MessageErrorLoadingSupporterInfo": "Det opstod en fejl ved indl\u00e6sning at supporter information. Pr\u00f8v igen senere.",
+    "MessageLinkYourSupporterKey": "Sammenk\u00e6d din supporter n\u00f8gle med op til {0} Emby Connect medlemmer for at f\u00e5 gratis adgang til disse apps:",
+    "HeaderConfirmRemoveUser": "Fjern bruger",
+    "MessageSwipeDownOnRemoteControl": "Velkommen til fjernstyring. V\u00e6lg hvilken enhed du vil styre ved at klikke cast ikonet i \u00f8verste h\u00f8jre hj\u00f8rne. Tr\u00e6k ned hvor som helst p\u00e5 sk\u00e6rmen for at g\u00e5 tilbage til hvor du kom fra.",
+    "MessageConfirmRemoveConnectSupporter": "Er du sikker p\u00e5 at du vil fjerne supporter fordelene fra denne bruger?",
+    "ValueTimeLimitSingleHour": "Tidsbegr\u00e6nsning: 1 time",
+    "ValueTimeLimitMultiHour": "Tidsbegr\u00e6nsning: {0} timer",
     "HeaderUsers": "Brugere",
-    "PluginCategoryGeneral": "General",
-    "PluginCategoryContentProvider": "Content Providers",
-    "PluginCategoryScreenSaver": "Screen Savers",
-    "PluginCategoryTheme": "Themes",
+    "PluginCategoryGeneral": "Generelt",
+    "PluginCategoryContentProvider": "Indholdsydbydere",
+    "PluginCategoryScreenSaver": "Pausesk\u00e6rme",
+    "PluginCategoryTheme": "Temaer",
     "PluginCategorySync": "Sync",
-    "PluginCategorySocialIntegration": "Social Networks",
-    "PluginCategoryNotifications": "Notifications",
+    "PluginCategorySocialIntegration": "Sociale netv\u00e6rk",
+    "PluginCategoryNotifications": "Underretninger",
     "PluginCategoryMetadata": "Metadata",
     "PluginCategoryLiveTV": "Live TV",
-    "PluginCategoryChannel": "Channels",
-    "HeaderSearch": "Search",
-    "ValueDateCreated": "Date created: {0}",
+    "PluginCategoryChannel": "Kanaler",
+    "HeaderSearch": "S\u00f8g",
+    "ValueDateCreated": "Oprettelsesdato: {0}",
     "LabelArtist": "Artist",
-    "LabelMovie": "Movie",
-    "LabelMusicVideo": "Music Video",
+    "LabelMovie": "Film",
+    "LabelMusicVideo": "Musikvideo",
     "LabelEpisode": "Episode",
     "LabelSeries": "Serier",
-    "LabelStopping": "Stopping",
-    "LabelCancelled": "(cancelled)",
+    "LabelStopping": "Standser",
+    "LabelCancelled": "(annulleret)",
     "LabelFailed": "(fejlede)",
-    "ButtonHelp": "Help",
+    "ButtonHelp": "Hj\u00e6lp",
     "ButtonSave": "Gem",
-    "ButtonDownload": "Download",
-    "SyncJobStatusQueued": "Queued",
-    "SyncJobStatusConverting": "Converting",
-    "SyncJobStatusFailed": "Failed",
-    "SyncJobStatusCancelled": "Cancelled",
-    "SyncJobStatusCompleted": "Synced",
-    "SyncJobStatusReadyToTransfer": "Ready to Transfer",
-    "SyncJobStatusTransferring": "Transferring",
-    "SyncJobStatusCompletedWithError": "Synced with errors",
-    "SyncJobItemStatusReadyToTransfer": "Ready to Transfer",
-    "LabelCollection": "Collection",
+    "ButtonDownload": "Hent",
+    "SyncJobStatusQueued": "Sat i k\u00f8",
+    "SyncJobStatusConverting": "Konverterer",
+    "SyncJobStatusFailed": "Fejlet",
+    "SyncJobStatusCancelled": "Annuleret",
+    "SyncJobStatusCompleted": "Synkroniseret",
+    "SyncJobStatusReadyToTransfer": "Klar til overf\u00f8rsel",
+    "SyncJobStatusTransferring": "Overf\u00f8rer",
+    "SyncJobStatusCompletedWithError": "Synkroniseret med fejl",
+    "SyncJobItemStatusReadyToTransfer": "Klar til overf\u00f8rsel",
+    "LabelCollection": "Samling",
     "HeaderAddToCollection": "Tilf\u00f8j til samling",
-    "NewCollectionNameExample": "Eksempel: Star Wars Collection",
-    "OptionSearchForInternetMetadata": "Search the internet for artwork and metadata",
-    "LabelSelectCollection": "Select collection:",
-    "HeaderDevices": "Devices",
-    "ButtonScheduledTasks": "Scheduled tasks",
-    "MessageItemsAdded": "Items added",
-    "ButtonAddToCollection": "Add to collection",
-    "HeaderSelectCertificatePath": "Select Certificate Path",
-    "ConfirmMessageScheduledTaskButton": "This operation normally runs automatically as a scheduled task. It can also be run manually here. To configure the scheduled task, see:",
-    "HeaderSupporterBenefit": "A supporter membership provides additional benefits such as access to sync, premium plugins, internet channel content, and more. {0}Learn more{1}.",
-    "LabelSyncNoTargetsHelp": "It looks like you don't currently have any apps that support sync.",
-    "HeaderWelcomeToProjectServerDashboard": "Welcome to the Emby Server Dashboard",
-    "HeaderWelcomeToProjectWebClient": "Welcome to the Emby Web Client",
-    "ButtonTakeTheTour": "Take the tour",
-    "HeaderWelcomeBack": "Welcome back!",
-    "TitlePlugins": "Plugins",
-    "ButtonTakeTheTourToSeeWhatsNew": "Take the tour to see what's new",
-    "MessageNoSyncJobsFound": "No sync jobs found. Create sync jobs using the Sync buttons found throughout the web interface.",
-    "HeaderLibraryAccess": "Library Access",
-    "HeaderChannelAccess": "Channel Access",
-    "HeaderDeviceAccess": "Device Access",
-    "HeaderSelectDevices": "Select Devices",
-    "ButtonCancelItem": "Cancel item",
-    "ButtonQueueForRetry": "Queue for retry",
-    "ButtonReenable": "Re-enable",
-    "ButtonLearnMore": "Learn more",
-    "SyncJobItemStatusSyncedMarkForRemoval": "Marked for removal",
-    "LabelAbortedByServerShutdown": "(Aborted by server shutdown)",
-    "LabelScheduledTaskLastRan": "Last ran {0}, taking {1}.",
-    "HeaderDeleteTaskTrigger": "Delete Task Trigger",
+    "NewCollectionNameExample": "Eksempel: Star Wars samling",
+    "OptionSearchForInternetMetadata": "S\u00f8g p\u00e5 internettet efter billeder og metadata",
+    "LabelSelectCollection": "V\u00e6lg samling:",
+    "HeaderDevices": "Enheder",
+    "ButtonScheduledTasks": "Planlagte opgaver",
+    "MessageItemsAdded": "Elementer tilf\u00f8jet",
+    "ButtonAddToCollection": "Tilf\u00f8j til samling",
+    "HeaderSelectCertificatePath": "V\u00e6lg certifikatsti",
+    "ConfirmMessageScheduledTaskButton": "Denne operation k\u00f8rer normalt som en planlagt opgave. Den kan ogs\u00e5 k\u00f8res manuelt her. For at konfigurere den planlage opgave, se:",
+    "HeaderSupporterBenefit": "Med et supporter medlemskab opn\u00e5r du ekstra fordele s\u00e5 som adgang til sync, premium plugins, internet kanaler samt meget mere. {0}L\u00e6r mere{1}.",
+    "LabelSyncNoTargetsHelp": "Det ser ud til at du for \u00f8jeblikket ikke har nogle enheder ser underst\u00f8tter sync.",
+    "HeaderWelcomeToProjectServerDashboard": "Velkommen til Emby betjeningspanel",
+    "HeaderWelcomeToProjectWebClient": "Velkommen til Emby",
+    "ButtonTakeTheTour": "Vis introduktion",
+    "HeaderWelcomeBack": "Velkommen tilbage!",
+    "TitlePlugins": "Tilf\u00f8jelser",
+    "ButtonTakeTheTourToSeeWhatsNew": "Tag en rundvisning for at se hvad der er nyt",
+    "MessageNoSyncJobsFound": "Intet sync job blev fundet. Opret sync jobs ved at benytte Sync knapper som findes gennem web-interfacet.",
+    "HeaderLibraryAccess": "Adgang til biblioteker",
+    "HeaderChannelAccess": "Adgang til kanaler",
+    "HeaderDeviceAccess": "Enhedsadgang",
+    "HeaderSelectDevices": "V\u00e6lg enheder",
+    "ButtonCancelItem": "Annuller genstand",
+    "ButtonQueueForRetry": "S\u00e6t et nyt fors\u00f8g i k\u00f8",
+    "ButtonReenable": "Genaktiver",
+    "ButtonLearnMore": "L\u00e6r mere",
+    "SyncJobItemStatusSyncedMarkForRemoval": "Markeret til sletning",
+    "LabelAbortedByServerShutdown": "(Annulleret grundet server nedlukning)",
+    "LabelScheduledTaskLastRan": "Sidst k\u00f8rt {0}, og tog {1}.",
+    "HeaderDeleteTaskTrigger": "Slet Task Trigger",
     "HeaderTaskTriggers": "Task Triggers",
-    "MessageDeleteTaskTrigger": "Are you sure you wish to delete this task trigger?",
-    "MessageNoPluginsInstalled": "You have no plugins installed.",
-    "LabelVersionInstalled": "{0} installed",
-    "LabelNumberReviews": "{0} Reviews",
-    "LabelFree": "Free",
-    "HeaderPlaybackError": "Playback Error",
-    "MessagePlaybackErrorNotAllowed": "You're currently not authorized to play this content. Please contact your system administrator for details.",
-    "MessagePlaybackErrorNoCompatibleStream": "No compatible streams are currently available. Please try again later or contact your system administrator for details.",
-    "MessagePlaybackErrorRateLimitExceeded": "Your playback rate limit has been exceeded. Please contact your system administrator for details.",
-    "MessagePlaybackErrorPlaceHolder": "The content chosen is not playable from this device.",
-    "HeaderSelectAudio": "Select Audio",
-    "HeaderSelectSubtitles": "Select Subtitles",
-    "ButtonMarkForRemoval": "Remove from device",
-    "ButtonUnmarkForRemoval": "Cancel removal from device",
-    "LabelDefaultStream": "(Default)",
-    "LabelForcedStream": "(Forced)",
-    "LabelDefaultForcedStream": "(Default\/Forced)",
-    "LabelUnknownLanguage": "Unknown language",
-    "MessageConfirmSyncJobItemCancellation": "Are you sure you wish to cancel this item?",
-    "ButtonMute": "Mute",
-    "ButtonUnmute": "Unmute",
+    "MessageDeleteTaskTrigger": "Er du sikker p\u00e5 du \u00f8nsker at slette denne task trigger?",
+    "MessageNoPluginsInstalled": "Du har ingen plugins installeret.",
+    "LabelVersionInstalled": "{0} installeret",
+    "LabelNumberReviews": "{0} Anmeldelser",
+    "LabelFree": "Gratis",
+    "HeaderPlaybackError": "Fejl i afspilning",
+    "MessagePlaybackErrorNotAllowed": "Du er p\u00e5 nuv\u00e6rende tidspunkt ikke autoriseret til at afspille dette indhold. Kontakt venligst din systemadministrator for flere detaljer.",
+    "MessagePlaybackErrorNoCompatibleStream": "Ingen kompatible streams er tilg\u00e6ngelige p\u00e5 nuv\u00e6rende tidspunkt. Pr\u00f8v igen senere eller kontakt din systemadministrator for flere detaljer.",
+    "MessagePlaybackErrorRateLimitExceeded": "Din afspilningskvote er blevet overskredet. Kontakt venligst din systemadministrator for flere detaljer.",
+    "MessagePlaybackErrorPlaceHolder": "Det valgte indhold kan ikke afspilles fra denne enhed.",
+    "HeaderSelectAudio": "V\u00e6lg lydspor",
+    "HeaderSelectSubtitles": "V\u00e6lg undertekster",
+    "ButtonMarkForRemoval": "Fjern fra enhed",
+    "ButtonUnmarkForRemoval": "Annuller fjernelse fra enhed",
+    "LabelDefaultStream": "(Standard)",
+    "LabelForcedStream": "(Tvungen)",
+    "LabelDefaultForcedStream": "(Standard\/Tvungen)",
+    "LabelUnknownLanguage": "Ukendt sprog",
+    "MessageConfirmSyncJobItemCancellation": "Er du sikker p\u00e5 du \u00f8nsker at annullere denne genstand?",
+    "ButtonMute": "Lyd fra",
+    "ButtonUnmute": "Sl\u00e5 lyd til",
     "ButtonStop": "Stop",
-    "ButtonNextTrack": "Next Track",
+    "ButtonNextTrack": "N\u00e6ste spor",
     "ButtonPause": "Pause",
     "ButtonPlay": "Afspil",
     "ButtonEdit": "Rediger",
-    "ButtonQueue": "Queue",
+    "ButtonQueue": "K\u00f8",
     "ButtonPlayTrailer": "Afspil trailer",
-    "ButtonPlaylist": "Playlist",
-    "ButtonPreviousTrack": "Previous Track",
-    "LabelEnabled": "Enabled",
-    "LabelDisabled": "Disabled",
-    "ButtonMoreInformation": "More Information",
-    "LabelNoUnreadNotifications": "No unread notifications.",
-    "ButtonViewNotifications": "View notifications",
-    "ButtonMarkTheseRead": "Mark these read",
-    "ButtonClose": "Close",
-    "LabelAllPlaysSentToPlayer": "All plays will be sent to the selected player.",
-    "MessageInvalidUser": "Invalid username or password. Please try again.",
-    "HeaderLoginFailure": "Login Failure",
-    "HeaderAllRecordings": "Alle Optagelser",
-    "RecommendationBecauseYouLike": "Because you like {0}",
-    "RecommendationBecauseYouWatched": "Because you watched {0}",
-    "RecommendationDirectedBy": "Directed by {0}",
-    "RecommendationStarring": "Starring {0}",
-    "HeaderConfirmRecordingCancellation": "Confirm Recording Cancellation",
-    "MessageConfirmRecordingCancellation": "Are you sure you wish to cancel this recording?",
-    "MessageRecordingCancelled": "Recording cancelled.",
-    "HeaderConfirmSeriesCancellation": "Confirm Series Cancellation",
-    "MessageConfirmSeriesCancellation": "Are you sure you wish to cancel this series?",
-    "MessageSeriesCancelled": "Series cancelled.",
-    "HeaderConfirmRecordingDeletion": "Confirm Recording Deletion",
-    "MessageConfirmRecordingDeletion": "Are you sure you wish to delete this recording?",
-    "MessageRecordingDeleted": "Recording deleted.",
-    "ButonCancelRecording": "Cancel Recording",
-    "MessageRecordingSaved": "Recording saved.",
+    "ButtonPlaylist": "Afspilningsliste",
+    "ButtonPreviousTrack": "Forrige spor",
+    "LabelEnabled": "Sl\u00e5et til",
+    "LabelDisabled": "Sl\u00e5et fra",
+    "ButtonMoreInformation": "Mere information",
+    "LabelNoUnreadNotifications": "Ingen ul\u00e6ste notifikationer",
+    "ButtonViewNotifications": "Se notifikationer",
+    "ButtonMarkTheseRead": "Marker disse som l\u00e6st",
+    "ButtonClose": "Luk",
+    "LabelAllPlaysSentToPlayer": "Alle afspilninger vil blive sendt til den valgte afspiller.",
+    "MessageInvalidUser": "Ukendt brugernavn eller adgangskode. Pr\u00f8v igen.",
+    "HeaderLoginFailure": "Login fejl",
+    "HeaderAllRecordings": "Alle optagelser",
+    "RecommendationBecauseYouLike": "Fordi du kan lide {0}",
+    "RecommendationBecauseYouWatched": "Fordi du har set {0}",
+    "RecommendationDirectedBy": "Instrueret af {0}",
+    "RecommendationStarring": "Hovedrolleindehavere {0}",
+    "HeaderConfirmRecordingCancellation": "Bekr\u00e6ft annullering af optagelse",
+    "MessageConfirmRecordingCancellation": "Er du sikker p\u00e5 du \u00f8nsker at annullere denne optagelse?",
+    "MessageRecordingCancelled": "Optagelse annulleret.",
+    "HeaderConfirmSeriesCancellation": "Bekr\u00e6ft annullering af serie",
+    "MessageConfirmSeriesCancellation": "Er du sikker p\u00e5 du \u00f8nsker at annullere denne serie?",
+    "MessageSeriesCancelled": "Serie annulleret.",
+    "HeaderConfirmRecordingDeletion": "Bekr\u00e6ft sletning af optagelse",
+    "MessageConfirmRecordingDeletion": "Er du sikker p\u00e5 du \u00f8nsker at slette denne optagelse?",
+    "MessageRecordingDeleted": "Optagelse slettet.",
+    "ButonCancelRecording": "Annuller optagelse",
+    "MessageRecordingSaved": "Optagelse gemt.",
     "OptionSunday": "S\u00f8ndag",
     "OptionMonday": "Mandag",
     "OptionTuesday": "Tirsdag",
@@ -179,333 +180,333 @@
     "OptionThursday": "Torsdag",
     "OptionFriday": "Fredag",
     "OptionSaturday": "L\u00f8rdag",
-    "OptionEveryday": "Every day",
-    "OptionWeekend": "Weekends",
-    "OptionWeekday": "Weekdays",
-    "HeaderConfirmDeletion": "Confirm Deletion",
-    "MessageConfirmPathSubstitutionDeletion": "Are you sure you wish to delete this path substitution?",
-    "LiveTvUpdateAvailable": "(Update available)",
-    "LabelVersionUpToDate": "Up to date!",
+    "OptionEveryday": "Hver dag",
+    "OptionWeekend": "Weekender",
+    "OptionWeekday": "Hverdage",
+    "HeaderConfirmDeletion": "Bekr\u00e6ft sletning",
+    "MessageConfirmPathSubstitutionDeletion": "Er du sikker p\u00e5 du \u00f8nsker at slette denne stisubstitution?",
+    "LiveTvUpdateAvailable": "(Opdatering tilg\u00e6ngelig)",
+    "LabelVersionUpToDate": "Opdateret!",
     "ButtonResetTuner": "Reset tuner",
-    "HeaderResetTuner": "Reset Tuner",
-    "MessageConfirmResetTuner": "Are you sure you wish to reset this tuner? Any active players or recordings will be abruptly stopped.",
-    "ButtonCancelSeries": "Cancel Series",
-    "HeaderSeriesRecordings": "Series Recordings",
-    "LabelAnytime": "Any time",
-    "StatusRecording": "Recording",
-    "StatusWatching": "Watching",
-    "StatusRecordingProgram": "Recording {0}",
-    "StatusWatchingProgram": "Watching {0}",
-    "HeaderSplitMedia": "Split Media Apart",
-    "MessageConfirmSplitMedia": "Are you sure you wish to split the media sources into separate items?",
-    "HeaderError": "Error",
-    "MessageChromecastConnectionError": "Your Chromecast receiver is unable to connect to your Emby Server. Please check their connections and try again.",
-    "MessagePleaseSelectOneItem": "Please select at least one item.",
-    "MessagePleaseSelectTwoItems": "Please select at least two items.",
-    "MessageTheFollowingItemsWillBeGrouped": "The following titles will be grouped into one item:",
-    "MessageConfirmItemGrouping": "Emby apps will automatically choose the optimal version to play based on device and network performance. Are you sure you wish to continue?",
+    "HeaderResetTuner": "Reset tuner",
+    "MessageConfirmResetTuner": "Er du sikker p\u00e5 du \u00f8nsker at resette denne tuner? Alle aktive afspilninger eller optagelser vil stoppe pludseligt.",
+    "ButtonCancelSeries": "Annuller serie",
+    "HeaderSeriesRecordings": "Serieoptagelser",
+    "LabelAnytime": "Alle tidspunkter",
+    "StatusRecording": "Optagelse",
+    "StatusWatching": "Ser",
+    "StatusRecordingProgram": "Optager {0}",
+    "StatusWatchingProgram": "Ser {0}",
+    "HeaderSplitMedia": "Opsplit medie",
+    "MessageConfirmSplitMedia": "Er du sikker p\u00e5 du \u00f8nsker at opsplitte mediekilderne til separate klilder?",
+    "HeaderError": "Fejl",
+    "MessageChromecastConnectionError": "Din Chromecast modtager kan ikke forbinde til din Emby Server. Tjek venligst deres forbindelse og pr\u00f8v igen.",
+    "MessagePleaseSelectOneItem": "V\u00e6lg venligst mindst \u00e9t element.",
+    "MessagePleaseSelectTwoItems": "V\u00e6lg venligst mindst to elementer.",
+    "MessageTheFollowingItemsWillBeGrouped": "F\u00f8lgende elementer vil blive grupperet til et element:",
+    "MessageConfirmItemGrouping": "Emby apps vil automatisk fors\u00f8ge at afspille den optimale version baseret p\u00e5 enheden og netv\u00e6rksydelse. Er du sikker p\u00e5 du \u00f8nsker at forts\u00e6tte?",
     "HeaderResume": "Fors\u00e6t",
-    "HeaderMyViews": "My Views",
-    "HeaderLibraryFolders": "Media Folders",
-    "HeaderLatestMedia": "Latest Media",
-    "ButtonMoreItems": "More...",
-    "ButtonMore": "More",
-    "HeaderFavoriteMovies": "Favorite Movies",
-    "HeaderFavoriteShows": "Favorite Shows",
-    "HeaderFavoriteEpisodes": "Favorite Episodes",
-    "HeaderFavoriteGames": "Favorite Games",
-    "HeaderRatingsDownloads": "Rating \/ Downloads",
-    "HeaderConfirmProfileDeletion": "Confirm Profile Deletion",
-    "MessageConfirmProfileDeletion": "Are you sure you wish to delete this profile?",
-    "HeaderSelectServerCachePath": "Select Server Cache Path",
-    "HeaderSelectTranscodingPath": "Select Transcoding Temporary Path",
-    "HeaderSelectImagesByNamePath": "Select Images By Name Path",
-    "HeaderSelectMetadataPath": "Select Metadata Path",
-    "HeaderSelectServerCachePathHelp": "Browse or enter the path to use for server cache files. The folder must be writeable.",
-    "HeaderSelectTranscodingPathHelp": "Browse or enter the path to use for transcoding temporary files. The folder must be writeable.",
-    "HeaderSelectImagesByNamePathHelp": "Browse or enter the path to your items by name folder. The folder must be writeable.",
-    "HeaderSelectMetadataPathHelp": "Browse or enter the path you'd like to store metadata within. The folder must be writeable.",
-    "HeaderSelectChannelDownloadPath": "Select Channel Download Path",
-    "HeaderSelectChannelDownloadPathHelp": "Browse or enter the path to use for storing channel cache files. The folder must be writeable.",
-    "OptionNewCollection": "New...",
+    "HeaderMyViews": "Mine visninger",
+    "HeaderLibraryFolders": "Mediemapper",
+    "HeaderLatestMedia": "Seneste medier",
+    "ButtonMoreItems": "Mere...",
+    "ButtonMore": "Mere",
+    "HeaderFavoriteMovies": "Favorit film",
+    "HeaderFavoriteShows": "Favorit serier",
+    "HeaderFavoriteEpisodes": "Favorit episoder",
+    "HeaderFavoriteGames": "Favorit spil",
+    "HeaderRatingsDownloads": "Bed\u00f8mmelser \/ Downloads",
+    "HeaderConfirmProfileDeletion": "Bekr\u00e6ft sletning af profil",
+    "MessageConfirmProfileDeletion": "Er du sikker p\u00e5 du \u00f8nsker at slette denne profil?",
+    "HeaderSelectServerCachePath": "V\u00e6lg \"Server Cache Path\"",
+    "HeaderSelectTranscodingPath": "V\u00e6lg \"Transcoding Temporary Path\"",
+    "HeaderSelectImagesByNamePath": "V\u00e6lg billeder efter navn-sti:",
+    "HeaderSelectMetadataPath": "V\u00e6lg Metadata Path",
+    "HeaderSelectServerCachePathHelp": "V\u00e6lg eller indtast stien som skal benyttes til serverens cache filer. Mappen m\u00e5 ikke v\u00e6re skrivebeskyttet.",
+    "HeaderSelectTranscodingPathHelp": "V\u00e6lg eller indtast stien som skal benyttes til midlertidige transkodningsfiler. Mappen m\u00e5 ikke v\u00e6re skrivebeskyttet.",
+    "HeaderSelectImagesByNamePathHelp": "V\u00e6lg eller indtast stien som f\u00f8rer til mappen med dine elmenter per navn. Mappen m\u00e5 ikke v\u00e6re skrivebeskyttet.",
+    "HeaderSelectMetadataPathHelp": "V\u00e6lg eller indtast stien for hvor du \u00f8nsker at gemme din metadata. Mappen m\u00e5 ikke v\u00e6re skrivebeskyttet.",
+    "HeaderSelectChannelDownloadPath": "V\u00e6lg sti for hentning af kanalindhold",
+    "HeaderSelectChannelDownloadPathHelp": "V\u00e6lg eller indtast stien for hvor du \u00f8nsker at gemme kanalindholds cache filer. Mappen m\u00e5 ikke v\u00e6re skrivebeskyttet.",
+    "OptionNewCollection": "Ny...",
     "ButtonAdd": "Tilf\u00f8j",
     "ButtonRemove": "Fjern",
-    "LabelChapterDownloaders": "Chapter downloaders:",
-    "LabelChapterDownloadersHelp": "Enable and rank your preferred chapter downloaders in order of priority. Lower priority downloaders will only be used to fill in missing information.",
-    "HeaderFavoriteAlbums": "Favorite Albums",
-    "HeaderLatestChannelMedia": "Latest Channel Items",
-    "ButtonOrganizeFile": "Organize File",
-    "ButtonDeleteFile": "Delete File",
-    "HeaderOrganizeFile": "Organize File",
-    "HeaderDeleteFile": "Delete File",
-    "StatusSkipped": "Skipped",
-    "StatusFailed": "Failed",
-    "StatusSuccess": "Success",
-    "MessageFileWillBeDeleted": "The following file will be deleted:",
-    "MessageSureYouWishToProceed": "Are you sure you wish to proceed?",
-    "MessageDuplicatesWillBeDeleted": "In addition the following dupliates will be deleted:",
-    "MessageFollowingFileWillBeMovedFrom": "The following file will be moved from:",
-    "MessageDestinationTo": "to:",
-    "HeaderSelectWatchFolder": "Select Watch Folder",
-    "HeaderSelectWatchFolderHelp": "Browse or enter the path to your watch folder. The folder must be writeable.",
-    "OrganizePatternResult": "Result: {0}",
-    "HeaderRestart": "Restart",
-    "HeaderShutdown": "Shutdown",
-    "MessageConfirmRestart": "Are you sure you wish to restart Emby Server?",
-    "MessageConfirmShutdown": "Are you sure you wish to shutdown Emby Server?",
-    "ButtonUpdateNow": "Update Now",
-    "ValueItemCount": "{0} item",
-    "ValueItemCountPlural": "{0} items",
-    "NewVersionOfSomethingAvailable": "A new version of {0} is available!",
-    "VersionXIsAvailableForDownload": "Version {0} is now available for download.",
+    "LabelChapterDownloaders": "Kapitel downloadere:",
+    "LabelChapterDownloadersHelp": "Aktiver og ranger dine fortrukne kapitel downloadere i en prioriteret r\u00e6kkef\u00f8lge. Lavt rangerende downloadere bliver kun benyttet til at udfylde manglende information.",
+    "HeaderFavoriteAlbums": "Favoritalbums",
+    "HeaderLatestChannelMedia": "Seneste kanalenheder",
+    "ButtonOrganizeFile": "Organiser fil",
+    "ButtonDeleteFile": "Slet fil",
+    "HeaderOrganizeFile": "Organiser fil",
+    "HeaderDeleteFile": "Slet fil",
+    "StatusSkipped": "Oversprunget",
+    "StatusFailed": "Fejlet",
+    "StatusSuccess": "Succes",
+    "MessageFileWillBeDeleted": "Den f\u00f8lgende fil vil blive slettet:",
+    "MessageSureYouWishToProceed": "\u00d8nsker du at forts\u00e6tte?",
+    "MessageDuplicatesWillBeDeleted": "Derudover vil f\u00f8lgende duplikater blive slettet:",
+    "MessageFollowingFileWillBeMovedFrom": "Den f\u00f8lgende fil vil blive flyttet fra:",
+    "MessageDestinationTo": "til:",
+    "HeaderSelectWatchFolder": "V\u00e6lg en Watch Folder",
+    "HeaderSelectWatchFolderHelp": "V\u00e6lg eller indtast stien til din \"watch folder\". Mappen m\u00e5 ikke v\u00e6re skrivebeskyttet.",
+    "OrganizePatternResult": "Resultat: {0}",
+    "HeaderRestart": "Genstart",
+    "HeaderShutdown": "Luk",
+    "MessageConfirmRestart": "Er du sikker p\u00e5 du \u00f8nsker at genstarte Emby?",
+    "MessageConfirmShutdown": "Er du sikker p\u00e5 du \u00f8nsker at lukke Emby?",
+    "ButtonUpdateNow": "Opdater nu",
+    "ValueItemCount": "{0} elment",
+    "ValueItemCountPlural": "{0} elementer",
+    "NewVersionOfSomethingAvailable": "En ny version af {0} er tilg\u00e6ngelig!",
+    "VersionXIsAvailableForDownload": "Version {0} kan nu downloades.",
     "LabelVersionNumber": "Version {0}",
-    "LabelPlayMethodTranscoding": "Transcoding",
-    "LabelPlayMethodDirectStream": "Direct Streaming",
-    "LabelPlayMethodDirectPlay": "Direct Playing",
-    "LabelAudioCodec": "Audio: {0}",
+    "LabelPlayMethodTranscoding": "Transkoding",
+    "LabelPlayMethodDirectStream": "Direkte streaming",
+    "LabelPlayMethodDirectPlay": "Direkte afspilning",
+    "LabelAudioCodec": "Lyd: {0}",
     "LabelVideoCodec": "Video: {0}",
-    "LabelLocalAccessUrl": "Local access: {0}",
-    "LabelRemoteAccessUrl": "Remote access: {0}",
-    "LabelRunningOnPort": "Running on http port {0}.",
-    "LabelRunningOnPorts": "Running on http port {0}, and https port {1}.",
-    "HeaderLatestFromChannel": "Latest from {0}",
-    "LabelUnknownLanaguage": "Unknown language",
-    "HeaderCurrentSubtitles": "Current Subtitles",
-    "MessageDownloadQueued": "The download has been queued.",
-    "MessageAreYouSureDeleteSubtitles": "Are you sure you wish to delete this subtitle file?",
-    "ButtonRemoteControl": "Remote Control",
-    "HeaderLatestTvRecordings": "Latest Recordings",
+    "LabelLocalAccessUrl": "Lokal adgang: {0}",
+    "LabelRemoteAccessUrl": "Fjernadgang: {0}",
+    "LabelRunningOnPort": "K\u00f8rer p\u00e5 http port {0}.",
+    "LabelRunningOnPorts": "K\u00f8rer p\u00e5 http port {0}, og https port {1}.",
+    "HeaderLatestFromChannel": "Seneste fra {0}",
+    "LabelUnknownLanaguage": "Ukendt sprog",
+    "HeaderCurrentSubtitles": "Nuv\u00e6rende undertekster",
+    "MessageDownloadQueued": "Downloadet er sat i k\u00f8.",
+    "MessageAreYouSureDeleteSubtitles": "Er du sikker p\u00e5 du \u00f8nsker at slette denne undertekstfil?",
+    "ButtonRemoteControl": "Fjernstyring",
+    "HeaderLatestTvRecordings": "Seneste optagelser",
     "ButtonOk": "Ok",
     "ButtonCancel": "Annuller",
-    "ButtonRefresh": "Refresh",
-    "LabelCurrentPath": "Current path:",
-    "HeaderSelectMediaPath": "Select Media Path",
-    "HeaderSelectPath": "Select Path",
-    "ButtonNetwork": "Network",
-    "MessageDirectoryPickerInstruction": "Network paths can be entered manually in the event the Network button fails to locate your devices. For example, {0} or {1}.",
+    "ButtonRefresh": "Opdater",
+    "LabelCurrentPath": "Nuv\u00e6rende sti:",
+    "HeaderSelectMediaPath": "V\u00e6lg mediesti",
+    "HeaderSelectPath": "V\u00e6lg sti",
+    "ButtonNetwork": "Netv\u00e6rk",
+    "MessageDirectoryPickerInstruction": "Netv\u00e6rksstier kan indtastes manuelt i tilf\u00e6lde af at netv\u00e6rksknappen ikke kan lokalisere dine enheder. Foreksempel, {0} eller {1}.",
     "HeaderMenu": "Menu",
-    "ButtonOpen": "Open",
-    "ButtonOpenInNewTab": "Open in new tab",
-    "ButtonShuffle": "Shuffle",
-    "ButtonInstantMix": "Instant mix",
-    "ButtonResume": "Resume",
-    "HeaderScenes": "Scenes",
-    "HeaderAudioTracks": "Audio Tracks",
-    "HeaderLibraries": "Libraries",
-    "HeaderSubtitles": "Subtitles",
-    "HeaderVideoQuality": "Video Quality",
-    "MessageErrorPlayingVideo": "There was an error playing the video.",
-    "MessageEnsureOpenTuner": "Please ensure there is an open tuner availalble.",
+    "ButtonOpen": "\u00c5ben",
+    "ButtonOpenInNewTab": "\u00c5ben i ny fane",
+    "ButtonShuffle": "Bland",
+    "ButtonInstantMix": "Instant Mix",
+    "ButtonResume": "Genoptag",
+    "HeaderScenes": "Scener",
+    "HeaderAudioTracks": "Lydspor",
+    "HeaderLibraries": "Bibliotekter",
+    "HeaderSubtitles": "Undertekster",
+    "HeaderVideoQuality": "Videokvalitet",
+    "MessageErrorPlayingVideo": "Der opstod en fejl under afspilning af videoen.",
+    "MessageEnsureOpenTuner": "Sikre dig at en \u00e5ben tuner er tilg\u00e6ngelig.",
     "ButtonHome": "Hjem",
-    "ButtonDashboard": "Dashboard",
-    "ButtonReports": "Reports",
+    "ButtonDashboard": "Betjeningspanel",
+    "ButtonReports": "Rapporter",
     "ButtonMetadataManager": "Metadata Manager",
-    "HeaderTime": "Time",
-    "HeaderName": "Name",
+    "HeaderTime": "Tid",
+    "HeaderName": "Navn",
     "HeaderAlbum": "Album",
-    "HeaderAlbumArtist": "Album Artist",
+    "HeaderAlbumArtist": "Album kunstner",
     "HeaderArtist": "Artist",
-    "LabelAddedOnDate": "Added {0}",
+    "LabelAddedOnDate": "Tilf\u00f8jet {0}",
     "ButtonStart": "Start",
     "HeaderChannels": "Kanaler",
-    "HeaderMediaFolders": "Media Folders",
-    "HeaderBlockItemsWithNoRating": "Block content with no rating information:",
-    "OptionBlockOthers": "Others",
-    "OptionBlockTvShows": "TV Shows",
-    "OptionBlockTrailers": "Trailers",
-    "OptionBlockMusic": "Music",
-    "OptionBlockMovies": "Movies",
-    "OptionBlockBooks": "Books",
-    "OptionBlockGames": "Games",
-    "OptionBlockLiveTvPrograms": "Live TV Programs",
-    "OptionBlockLiveTvChannels": "Live TV Channels",
-    "OptionBlockChannelContent": "Internet Channel Content",
-    "ButtonRevoke": "Revoke",
-    "MessageConfirmRevokeApiKey": "Are you sure you wish to revoke this api key? The application's connection to Emby Server will be abruptly terminated.",
-    "HeaderConfirmRevokeApiKey": "Revoke Api Key",
-    "ValueContainer": "Container: {0}",
-    "ValueAudioCodec": "Audio Codec: {0}",
+    "HeaderMediaFolders": "Mediemapper",
+    "HeaderBlockItemsWithNoRating": "Bloker indhold uden bed\u00f8mmelser:",
+    "OptionBlockOthers": "Andre",
+    "OptionBlockTvShows": "TV serier",
+    "OptionBlockTrailers": "Trailere",
+    "OptionBlockMusic": "Musik",
+    "OptionBlockMovies": "Film",
+    "OptionBlockBooks": "B\u00f8ger",
+    "OptionBlockGames": "Spil",
+    "OptionBlockLiveTvPrograms": "Live TV-programmer",
+    "OptionBlockLiveTvChannels": "Live TV-kanaler",
+    "OptionBlockChannelContent": "Internet kanalindhold",
+    "ButtonRevoke": "Invalider",
+    "MessageConfirmRevokeApiKey": "Er du sikker p\u00e5 du \u00f8nsker at invalidere denne api n\u00f8gle? Applikationens forbindelse til Emby vil blive afbrudt \u00f8jeblikkeligt.",
+    "HeaderConfirmRevokeApiKey": "Invalider Api n\u00f8gle",
+    "ValueContainer": "Beholder: {0}",
+    "ValueAudioCodec": "Audio codec: {0}",
     "ValueVideoCodec": "Video Codec: {0}",
     "ValueCodec": "Codec: {0}",
-    "ValueConditions": "Conditions: {0}",
-    "LabelAll": "All",
-    "HeaderDeleteImage": "Delete Image",
-    "MessageFileNotFound": "File not found.",
-    "MessageFileReadError": "An error occurred reading this file.",
-    "ButtonNextPage": "Next Page",
-    "ButtonPreviousPage": "Previous Page",
-    "ButtonMoveLeft": "Move left",
-    "ButtonMoveRight": "Move right",
-    "ButtonBrowseOnlineImages": "Browse online images",
-    "HeaderDeleteItem": "Delete Item",
-    "ConfirmDeleteItem": "Deleting this item will delete it from both the file system and your media library. Are you sure you wish to continue?",
-    "MessagePleaseEnterNameOrId": "Please enter a name or an external Id.",
-    "MessageValueNotCorrect": "The value entered is not correct. Please try again.",
-    "MessageItemSaved": "Item saved.",
-    "MessagePleaseAcceptTermsOfServiceBeforeContinuing": "Please accept the terms of service before continuing.",
+    "ValueConditions": "Forhold: {0}",
+    "LabelAll": "Alle",
+    "HeaderDeleteImage": "Slet billede",
+    "MessageFileNotFound": "Fil blev ikke fundet.",
+    "MessageFileReadError": "Der opstod en fejl i fors\u00f8get p\u00e5 at l\u00e6se filen.",
+    "ButtonNextPage": "N\u00e6ste side",
+    "ButtonPreviousPage": "Forrige side",
+    "ButtonMoveLeft": "Flyt til venstre",
+    "ButtonMoveRight": "Flyt til h\u00f8jre",
+    "ButtonBrowseOnlineImages": "Gennemse online billeder",
+    "HeaderDeleteItem": "Slet element",
+    "ConfirmDeleteItem": "Hvis dette element slettes, fjernes det b\u00e5de fra dit filsystem samt din mediebibliotek. Er du sikker p\u00e5 du \u00f8nsker at forts\u00e6tte?",
+    "MessagePleaseEnterNameOrId": "Indtast venligst et navn eller eksternt Id.",
+    "MessageValueNotCorrect": "Det indtastede v\u00e6rdi er ikke korrekt. Pr\u00f8v igen.",
+    "MessageItemSaved": "Element gemt.",
+    "MessagePleaseAcceptTermsOfServiceBeforeContinuing": "Accepter venligst tjenestevilk\u00e5rene f\u00f8r du forts\u00e6tter.",
     "OptionEnded": "F\u00e6rdig",
     "OptionContinuing": "Fors\u00e6ttes",
     "OptionOff": "Off",
     "OptionOn": "On",
     "ButtonSettings": "Indstillinger",
-    "ButtonUninstall": "Uninstall",
-    "HeaderFields": "Fields",
-    "HeaderFieldsHelp": "Slide a field to 'off' to lock it and prevent it's data from being changed.",
+    "ButtonUninstall": "Afinstaller",
+    "HeaderFields": "Felter",
+    "HeaderFieldsHelp": "\u00c6ndre et felt til \"afbrudt\" for at l\u00e5se det og forhindre dets data i at blive \u00e6ndret.",
     "HeaderLiveTV": "Live TV",
-    "MissingLocalTrailer": "Missing local trailer.",
-    "MissingPrimaryImage": "Missing primary image.",
-    "MissingBackdropImage": "Missing backdrop image.",
-    "MissingLogoImage": "Missing logo image.",
-    "MissingEpisode": "Missing episode.",
-    "OptionScreenshots": "Screenshots",
-    "OptionBackdrops": "Backdrops",
-    "OptionImages": "Images",
-    "OptionKeywords": "Keywords",
+    "MissingLocalTrailer": "Mangler lokal trailer.",
+    "MissingPrimaryImage": "Mangler prim\u00e6rt billede",
+    "MissingBackdropImage": "Mangler baggrundsbillede.",
+    "MissingLogoImage": "Mangler logo.",
+    "MissingEpisode": "Mangler episode.",
+    "OptionScreenshots": "Sk\u00e6rmbilleder",
+    "OptionBackdrops": "Baggrunde",
+    "OptionImages": "Billeder",
+    "OptionKeywords": "N\u00f8gleord",
     "OptionTags": "Tags",
-    "OptionStudios": "Studios",
-    "OptionName": "Name",
-    "OptionOverview": "Overview",
-    "OptionGenres": "Genres",
-    "OptionParentalRating": "Parental Rating",
-    "OptionPeople": "People",
+    "OptionStudios": "Studier",
+    "OptionName": "Navn",
+    "OptionOverview": "Oversigt",
+    "OptionGenres": "Genrer",
+    "OptionParentalRating": "Aldersgr\u00e6nse",
+    "OptionPeople": "Personer",
     "OptionRuntime": "Varighed",
-    "OptionProductionLocations": "Production Locations",
-    "OptionBirthLocation": "Birth Location",
-    "LabelAllChannels": "All channels",
-    "LabelLiveProgram": "LIVE",
-    "LabelNewProgram": "NEW",
-    "LabelPremiereProgram": "PREMIERE",
+    "OptionProductionLocations": "Produktionslokationer",
+    "OptionBirthLocation": "F\u00f8dselssted",
+    "LabelAllChannels": "Alle kanaler",
+    "LabelLiveProgram": "DIREKTE",
+    "LabelNewProgram": "NY",
+    "LabelPremiereProgram": "PR\u00c6MIERE",
     "LabelHDProgram": "HD",
-    "HeaderChangeFolderType": "Change Content Type",
-    "HeaderChangeFolderTypeHelp": "To change the type, please remove and rebuild the folder with the new type.",
-    "HeaderAlert": "Alert",
-    "MessagePleaseRestart": "Please restart to finish updating.",
-    "ButtonRestart": "Restart",
-    "MessagePleaseRefreshPage": "Please refresh this page to receive new updates from the server.",
-    "ButtonHide": "Hide",
-    "MessageSettingsSaved": "Settings saved.",
-    "ButtonSignOut": "Sign Out",
-    "ButtonMyProfile": "My Profile",
-    "ButtonMyPreferences": "My Preferences",
-    "MessageBrowserDoesNotSupportWebSockets": "This browser does not support web sockets. For a better experience, try a newer browser such as Chrome, Firefox, IE10+, Safari (iOS) or Opera.",
-    "LabelInstallingPackage": "Installing {0}",
-    "LabelPackageInstallCompleted": "{0} installation completed.",
-    "LabelPackageInstallFailed": "{0} installation failed.",
-    "LabelPackageInstallCancelled": "{0} installation cancelled.",
+    "HeaderChangeFolderType": "\u00c6ndre indholdstype",
+    "HeaderChangeFolderTypeHelp": "For at \u00e6ndre typen bedes du fjerne og gendanne mappen med den nye type.",
+    "HeaderAlert": "Advarsel",
+    "MessagePleaseRestart": "Genstart venligst for at afslutte opdateringen.",
+    "ButtonRestart": "Genstart",
+    "MessagePleaseRefreshPage": "Genindl\u00e6s venligst denne side for at modtage nye opdateringer fra serveren.",
+    "ButtonHide": "Gem",
+    "MessageSettingsSaved": "Indstillinger er gemt.",
+    "ButtonSignOut": "Log af",
+    "ButtonMyProfile": "Min profil",
+    "ButtonMyPreferences": "Mine indstillinger",
+    "MessageBrowserDoesNotSupportWebSockets": "Denne browser underst\u00f8tter ikke \"web sockets\". For en bedre oplevelse benyt da en nyere browser s\u00e5 som Chrome, Firefox, IE10+, Safari (iOS) eller Opera.",
+    "LabelInstallingPackage": "Installerer {0}",
+    "LabelPackageInstallCompleted": "{0} installation udf\u00f8rt.",
+    "LabelPackageInstallFailed": "{0} installationen mislykkedes.",
+    "LabelPackageInstallCancelled": "{0} installation afbrudt.",
     "TabServer": "Server",
-    "TabUsers": "Users",
-    "TabLibrary": "Library",
+    "TabUsers": "Brugere",
+    "TabLibrary": "Bibliotek",
     "TabMetadata": "Metadata",
     "TabDLNA": "DLNA",
     "TabLiveTV": "Live TV",
-    "TabAutoOrganize": "Auto-Organize",
-    "TabPlugins": "Plugins",
-    "TabAdvanced": "Advanceret",
-    "TabHelp": "Help",
-    "TabScheduledTasks": "Scheduled Tasks",
-    "ButtonFullscreen": "Fuld sk\u00e6rm",
-    "ButtonAudioTracks": "Audio Tracks",
+    "TabAutoOrganize": "Organiser automatisk",
+    "TabPlugins": "Tilf\u00f8jelser",
+    "TabAdvanced": "Avanceret",
+    "TabHelp": "Hj\u00e6lp",
+    "TabScheduledTasks": "Planlagte opgaver",
+    "ButtonFullscreen": "Fuldsk\u00e6rm",
+    "ButtonAudioTracks": "Lydpor",
     "ButtonSubtitles": "Undertekster",
     "ButtonScenes": "Scener",
-    "ButtonQuality": "Quality",
-    "HeaderNotifications": "Notifications",
-    "HeaderSelectPlayer": "Select Player:",
+    "ButtonQuality": "Kvalitet",
+    "HeaderNotifications": "Notifikationer",
+    "HeaderSelectPlayer": "V\u00e6lg afspiller:",
     "ButtonSelect": "V\u00e6lg",
     "ButtonNew": "Ny",
-    "MessageInternetExplorerWebm": "For best results with Internet Explorer please install the WebM playback plugin.",
-    "HeaderVideoError": "Video Error",
-    "ButtonAddToPlaylist": "Add to playlist",
-    "HeaderAddToPlaylist": "Add to Playlist",
+    "MessageInternetExplorerWebm": "For at opn\u00e5 de bedste resultater med Internet Explorer bedes du installere WebM afspilningstilf\u00f8jelsen.",
+    "HeaderVideoError": "Video fejl",
+    "ButtonAddToPlaylist": "Tilf\u00f8j til afspilningsliste",
+    "HeaderAddToPlaylist": "Tilf\u00f8j til afspilningsliste",
     "LabelName": "Navn:",
-    "ButtonSubmit": "Submit",
-    "LabelSelectPlaylist": "Playlist:",
-    "OptionNewPlaylist": "New playlist...",
+    "ButtonSubmit": "Indsend",
+    "LabelSelectPlaylist": "Afspilningsliste:",
+    "OptionNewPlaylist": "Ny afspilningsliste...",
     "MessageAddedToPlaylistSuccess": "Ok",
-    "ButtonView": "View",
-    "ButtonViewSeriesRecording": "View series recording",
-    "ValueOriginalAirDate": "Original air date: {0}",
-    "ButtonRemoveFromPlaylist": "Remove from playlist",
-    "HeaderSpecials": "Specials",
-    "HeaderTrailers": "Trailers",
-    "HeaderAudio": "Audio",
-    "HeaderResolution": "Resolution",
+    "ButtonView": "Visning",
+    "ButtonViewSeriesRecording": "Vis serieoptagelse",
+    "ValueOriginalAirDate": "Blev sendt f\u00f8rste gang: {0}",
+    "ButtonRemoveFromPlaylist": "Fjer fra afspilningsliste",
+    "HeaderSpecials": "S\u00e6rudsendelser",
+    "HeaderTrailers": "Trailere",
+    "HeaderAudio": "Lyd",
+    "HeaderResolution": "Opl\u00f8sning",
     "HeaderVideo": "Video",
-    "HeaderRuntime": "Runtime",
-    "HeaderCommunityRating": "Community rating",
-    "HeaderPasswordReset": "Password Reset",
-    "HeaderParentalRating": "Parental rating",
-    "HeaderReleaseDate": "Release date",
-    "HeaderDateAdded": "Date added",
-    "HeaderSeries": "Series",
-    "HeaderSeason": "Season",
-    "HeaderSeasonNumber": "Season number",
-    "HeaderNetwork": "Network",
-    "HeaderYear": "Year",
-    "HeaderGameSystem": "Game system",
-    "HeaderPlayers": "Players",
-    "HeaderEmbeddedImage": "Embedded image",
-    "HeaderTrack": "Track",
-    "HeaderDisc": "Disc",
+    "HeaderRuntime": "Varighed",
+    "HeaderCommunityRating": "F\u00e6llesskabsvurdering",
+    "HeaderPasswordReset": "Nulstil adgangskode",
+    "HeaderParentalRating": "Aldersgr\u00e6nse",
+    "HeaderReleaseDate": "Udgivelsesdato",
+    "HeaderDateAdded": "Dato for tilf\u00f8jelse",
+    "HeaderSeries": "Serier",
+    "HeaderSeason": "S\u00e6son",
+    "HeaderSeasonNumber": "S\u00e6sonnummer",
+    "HeaderNetwork": "Netv\u00e6rk",
+    "HeaderYear": "\u00c5r",
+    "HeaderGameSystem": "Spilsystem",
+    "HeaderPlayers": "Afspillere",
+    "HeaderEmbeddedImage": "Indlejret billede",
+    "HeaderTrack": "Spor",
+    "HeaderDisc": "Disk",
     "OptionMovies": "Film",
-    "OptionCollections": "Collections",
-    "OptionSeries": "Series",
-    "OptionSeasons": "Seasons",
+    "OptionCollections": "Samlinger",
+    "OptionSeries": "Serier",
+    "OptionSeasons": "S\u00e6soner",
     "OptionEpisodes": "Episoder",
-    "OptionGames": "Games",
-    "OptionGameSystems": "Game systems",
-    "OptionMusicArtists": "Music artists",
-    "OptionMusicAlbums": "Music albums",
-    "OptionMusicVideos": "Music videos",
-    "OptionSongs": "Songs",
-    "OptionHomeVideos": "Home videos",
-    "OptionBooks": "Books",
-    "OptionAdultVideos": "Adult videos",
-    "ButtonUp": "Up",
-    "ButtonDown": "Down",
-    "LabelMetadataReaders": "Metadata readers:",
-    "LabelMetadataReadersHelp": "Rank your preferred local metadata sources in order of priority. The first file found will be read.",
-    "LabelMetadataDownloaders": "Metadata downloaders:",
-    "LabelMetadataDownloadersHelp": "Enable and rank your preferred metadata downloaders in order of priority. Lower priority downloaders will only be used to fill in missing information.",
-    "LabelMetadataSavers": "Metadata savers:",
-    "LabelMetadataSaversHelp": "Choose the file formats to save your metadata to.",
-    "LabelImageFetchers": "Image fetchers:",
-    "LabelImageFetchersHelp": "Enable and rank your preferred image fetchers in order of priority.",
-    "ButtonQueueAllFromHere": "Queue all from here",
-    "ButtonPlayAllFromHere": "Play all from here",
+    "OptionGames": "Spil",
+    "OptionGameSystems": "Spilsystemer",
+    "OptionMusicArtists": "Musikartister",
+    "OptionMusicAlbums": "Musikalbummer",
+    "OptionMusicVideos": "Musikvideoer",
+    "OptionSongs": "Sange",
+    "OptionHomeVideos": "Hjemmevideoer",
+    "OptionBooks": "B\u00f8ger",
+    "OptionAdultVideos": "Voksenfilm",
+    "ButtonUp": "Op",
+    "ButtonDown": "Ned",
+    "LabelMetadataReaders": "Metadata afl\u00e6sere:",
+    "LabelMetadataReadersHelp": "Ranger dine fortrukne lokale metadatakilder i prioriteret r\u00e6kkef\u00f8lge. Den f\u00f8rst fundne fil vil blive afl\u00e6st.",
+    "LabelMetadataDownloaders": "Metadata downloadere:",
+    "LabelMetadataDownloadersHelp": "Aktiver og ranger dine fortrukne metadata downloadere i en prioriteret r\u00e6kkef\u00f8lge. Lavt rangerende downloadere bliver kun benyttet til at udfylde manglende information.",
+    "LabelMetadataSavers": "Metadata-gemmer:",
+    "LabelMetadataSaversHelp": "V\u00e6lg de filformater du \u00f8nsker din metadata gemmes som.",
+    "LabelImageFetchers": "Billede-henter:",
+    "LabelImageFetchersHelp": "Aktiver og ranger dine fortrukne billede-hentere i en prioriteret r\u00e6kkef\u00f8lge.",
+    "ButtonQueueAllFromHere": "Set alt her i k\u00f8",
+    "ButtonPlayAllFromHere": "Afspil alt fra her",
     "LabelDynamicExternalId": "{0} Id:",
-    "HeaderIdentify": "Identify Item",
+    "HeaderIdentify": "Identificer genstand",
     "PersonTypePerson": "Person",
-    "LabelTitleDisplayOrder": "Title display order:",
-    "OptionSortName": "Sort name",
+    "LabelTitleDisplayOrder": "Titelvisningsorden:",
+    "OptionSortName": "Sorteringsnavn",
     "OptionReleaseDate": "Udgivelsesdato",
-    "LabelSeasonNumber": "Season number:",
-    "LabelDiscNumber": "Disc number",
-    "LabelParentNumber": "Parent number",
-    "LabelEpisodeNumber": "Episode number:",
-    "LabelTrackNumber": "Track number:",
-    "LabelNumber": "Number:",
-    "LabelReleaseDate": "Release date:",
-    "LabelEndDate": "End date:",
-    "LabelYear": "Year:",
-    "LabelDateOfBirth": "Date of birth:",
-    "LabelBirthYear": "Birth year:",
-    "LabelBirthDate": "Birth date:",
-    "LabelDeathDate": "Death date:",
-    "HeaderRemoveMediaLocation": "Remove Media Location",
-    "MessageConfirmRemoveMediaLocation": "Are you sure you wish to remove this location?",
-    "HeaderRenameMediaFolder": "Rename Media Folder",
-    "LabelNewName": "New name:",
-    "HeaderAddMediaFolder": "Add Media Folder",
-    "HeaderAddMediaFolderHelp": "Name (Movies, Music, TV, etc):",
-    "HeaderRemoveMediaFolder": "Remove Media Folder",
-    "MessageTheFollowingLocationWillBeRemovedFromLibrary": "The following media locations will be removed from your library:",
-    "MessageAreYouSureYouWishToRemoveMediaFolder": "Are you sure you wish to remove this media folder?",
-    "ButtonRename": "Rename",
-    "ButtonChangeType": "Change type",
-    "HeaderMediaLocations": "Media Locations",
-    "LabelContentTypeValue": "Content type: {0}",
-    "LabelPathSubstitutionHelp": "Optional: Path substitution can map server paths to network shares that clients can access for direct playback.",
-    "FolderTypeUnset": "Unset (mixed content)",
+    "LabelSeasonNumber": "S\u00e6sonnummer",
+    "LabelDiscNumber": "Disk nummer",
+    "LabelParentNumber": "Parent nummer",
+    "LabelEpisodeNumber": "Episodenummer",
+    "LabelTrackNumber": "Spor nummer:",
+    "LabelNumber": "Nummer:",
+    "LabelReleaseDate": "Udgivelsesdato:",
+    "LabelEndDate": "Slutdato:",
+    "LabelYear": "\u00c5r:",
+    "LabelDateOfBirth": "F\u00f8dselsdato:",
+    "LabelBirthYear": "F\u00f8dsels\u00e5r:",
+    "LabelBirthDate": "F\u00f8dselsdato:",
+    "LabelDeathDate": "D\u00f8dsdato:",
+    "HeaderRemoveMediaLocation": "Fjern medielokalisation",
+    "MessageConfirmRemoveMediaLocation": "Er du sikker p\u00e5 du \u00f8nsker at fjerne denne lokalisation?",
+    "HeaderRenameMediaFolder": "Omd\u00f8b mediemappe",
+    "LabelNewName": "Nyt navn:",
+    "HeaderAddMediaFolder": "Tilf\u00f8j mediemappe",
+    "HeaderAddMediaFolderHelp": "Navn (Film, Musik, TV, osv.):",
+    "HeaderRemoveMediaFolder": "Fjern mediemappe",
+    "MessageTheFollowingLocationWillBeRemovedFromLibrary": "F\u00f8lgende medielokationer vil blive fjerne fra dit bibliotek:",
+    "MessageAreYouSureYouWishToRemoveMediaFolder": "Er du sikker p\u00e5 du \u00f8nsker at fjerne denne mediemappe?",
+    "ButtonRename": "Omd\u00f8b",
+    "ButtonChangeType": "\u00c6ndre type",
+    "HeaderMediaLocations": "Medielokationer",
+    "LabelContentTypeValue": "Indholdstype: {0}",
+    "LabelPathSubstitutionHelp": "Valgfri: Stisubstitution kan sammenk\u00e6de serverstier til netv\u00e6rksstier som klienter derved kan tilg\u00e5 for direkte afspilning.",
+    "FolderTypeUnset": "Ikke valgt (blandet indhold)",
     "FolderTypeMovies": "FIlm",
     "FolderTypeMusic": "Musik",
     "FolderTypeAdultVideos": "Voksenfilm",
@@ -522,245 +523,248 @@
     "TabGames": "Spil",
     "TabAlbums": "Albums",
     "TabSongs": "Sange",
-    "TabMusicVideos": "Musik Videoer",
-    "BirthPlaceValue": "Birth place: {0}",
-    "DeathDateValue": "Died: {0}",
-    "BirthDateValue": "Born: {0}",
-    "HeaderLatestReviews": "Latest Reviews",
-    "HeaderPluginInstallation": "Plugin Installation",
-    "MessageAlreadyInstalled": "This version is already installed.",
-    "ValueReviewCount": "{0} Reviews",
-    "MessageYouHaveVersionInstalled": "You currently have version {0} installed.",
-    "MessageTrialExpired": "The trial period for this feature has expired",
-    "MessageTrialWillExpireIn": "The trial period for this feature will expire in {0} day(s)",
-    "MessageInstallPluginFromApp": "This plugin must be installed from with in the app you intend to use it in.",
-    "ValuePriceUSD": "Price: {0} (USD)",
-    "MessageFeatureIncludedWithSupporter": "You are registered for this feature, and will be able to continue using it with an active supporter membership.",
-    "MessageChangeRecurringPlanConfirm": "After completing this transaction you will need to cancel your previous recurring donation from within your PayPal account. Thank you for supporting Emby.",
-    "MessageSupporterMembershipExpiredOn": "Your supporter membership expired on {0}.",
-    "MessageYouHaveALifetimeMembership": "You have a lifetime supporter membership. You can provide additional donations on a one-time or recurring basis using the options below. Thank you for supporting Emby.",
-    "MessageYouHaveAnActiveRecurringMembership": "You have an active {0} membership. You can upgrade your plan using the options below.",
+    "TabMusicVideos": "Musikvideoer",
+    "BirthPlaceValue": "F\u00f8dselssted: {0}",
+    "DeathDateValue": "D\u00f8dsdato: {0}",
+    "BirthDateValue": "F\u00f8dt: {0}",
+    "HeaderLatestReviews": "Seneste anmeldeser",
+    "HeaderPluginInstallation": "Plugin installation",
+    "MessageAlreadyInstalled": "Denne version er allerede installeret.",
+    "ValueReviewCount": "{0} Anmeldelser",
+    "MessageYouHaveVersionInstalled": "Du har version {0} installeret.",
+    "MessageTrialExpired": "Pr\u00f8veperioden for denne funktion er udl\u00f8bet",
+    "MessageTrialWillExpireIn": "Pr\u00f8veperioden for denne funktion udl\u00f8ber om {0} dag(e)",
+    "MessageInstallPluginFromApp": "Dette plugin skal v\u00e6re installeret inde i den app du \u00f8nsker at benytte det fra.",
+    "ValuePriceUSD": "Pris: {0} (USD)",
+    "MessageFeatureIncludedWithSupporter": "Du er registreret til at benytte denne funktion, og kan blive ved med at benytte den under foruds\u00e6tning af et aktivt supporter medlemsskab.",
+    "MessageChangeRecurringPlanConfirm": "Efter denne transaktion er udf\u00f8rt skal du afmelde din tidligere l\u00f8bende donation inde fra din PayPal konto. Tak fordi du st\u00f8tter Emby.",
+    "MessageSupporterMembershipExpiredOn": "Dit supporter medlemskab udl\u00f8b den {0}.",
+    "MessageYouHaveALifetimeMembership": "Du har et livstidsmedlemskab. Du kan give yderligere donationer via en engangsydelse eller p\u00e5 l\u00f8bende basis ved at benytte mulighederne nedenfor. Tak fordi du st\u00f8tter Emby.",
+    "MessageYouHaveAnActiveRecurringMembership": "Du har et aktivt {0} medlemsskab. Du kan opgradere dette via mulighederne nedenfor.",
     "ButtonDelete": "Slet",
-    "HeaderEmbyAccountAdded": "Emby Account Added",
-    "MessageEmbyAccountAdded": "The Emby account has been added to this user.",
-    "MessagePendingEmbyAccountAdded": "The Emby account has been added to this user. An email will be sent to the owner of the account. The invitation will need to be confirmed by clicking a link within the email.",
-    "HeaderEmbyAccountRemoved": "Emby Account Removed",
-    "MessageEmbyAccontRemoved": "The Emby account has been removed from this user.",
-    "TooltipLinkedToEmbyConnect": "Linked to Emby Connect",
-    "HeaderUnrated": "Unrated",
-    "ValueDiscNumber": "Disc {0}",
-    "HeaderUnknownDate": "Unknown Date",
-    "HeaderUnknownYear": "Unknown Year",
+    "HeaderEmbyAccountAdded": "Emby konto tilf\u00f8jet",
+    "MessageEmbyAccountAdded": "Emby kontoen er blevet tilf\u00f8jet til denne bruger.",
+    "MessagePendingEmbyAccountAdded": "Emby kontoen er blevet tilf\u00f8jet denne bruger. En email sendes til ejeren af kontoen. Invitationen skal bekr\u00e6ftes ved at klikke p\u00e5 linket i emailen.",
+    "HeaderEmbyAccountRemoved": "Emby konto fjernet",
+    "MessageEmbyAccontRemoved": "Emby kontoen er blevet fjernet fra denne bruger.",
+    "TooltipLinkedToEmbyConnect": "Koblet til Emby Connect",
+    "HeaderUnrated": "Ingen bed\u00f8mmelse",
+    "ValueDiscNumber": "Disk {0}",
+    "HeaderUnknownDate": "Ukendt dato",
+    "HeaderUnknownYear": "Ukendt \u00e5r",
     "ValueMinutes": "{0} min",
-    "ButtonPlayExternalPlayer": "Play with external player",
-    "HeaderSelectExternalPlayer": "Select External Player",
-    "HeaderExternalPlayerPlayback": "External Player Playback",
-    "ButtonImDone": "I'm Done",
-    "OptionWatched": "Watched",
-    "OptionUnwatched": "Unwatched",
-    "ExternalPlayerPlaystateOptionsHelp": "Specify how you would like to resume playing this video next time.",
-    "LabelMarkAs": "Mark as:",
-    "OptionInProgress": "In-Progress",
-    "LabelResumePoint": "Resume point:",
-    "ValueOneMovie": "1 movie",
-    "ValueMovieCount": "{0} movies",
+    "ButtonPlayExternalPlayer": "Afspil med ekstern afspiller",
+    "HeaderSelectExternalPlayer": "V\u00e6lg ekstern afspiller",
+    "HeaderExternalPlayerPlayback": "Ekstern afspiller afspilning",
+    "ButtonImDone": "Jeg er f\u00e6rdig",
+    "OptionWatched": "Set",
+    "OptionUnwatched": "Ikke set",
+    "ExternalPlayerPlaystateOptionsHelp": "Specificer hvordan du gerne vil genoptage afspilningen af denne video n\u00e6ste gang.",
+    "LabelMarkAs": "Marker som:",
+    "OptionInProgress": "I gang",
+    "LabelResumePoint": "Genoptagelsespunkt:",
+    "ValueOneMovie": "1 film",
+    "ValueMovieCount": "{0} film",
     "ValueOneTrailer": "1 trailer",
-    "ValueTrailerCount": "{0} trailers",
-    "ValueOneSeries": "1 series",
-    "ValueSeriesCount": "{0} series",
+    "ValueTrailerCount": "{0} trailere",
+    "ValueOneSeries": "1 serie",
+    "ValueSeriesCount": "{0} serier",
     "ValueOneEpisode": "1 episode",
-    "ValueEpisodeCount": "{0} episodes",
-    "ValueOneGame": "1 game",
-    "ValueGameCount": "{0} games",
+    "ValueEpisodeCount": "{0} episoder",
+    "ValueOneGame": "1 spil",
+    "ValueGameCount": "{0} spil",
     "ValueOneAlbum": "1 album",
-    "ValueAlbumCount": "{0} albums",
-    "ValueOneSong": "1 song",
-    "ValueSongCount": "{0} songs",
-    "ValueOneMusicVideo": "1 music video",
-    "ValueMusicVideoCount": "{0} music videos",
+    "ValueAlbumCount": "{0} album",
+    "ValueOneSong": "1 sang",
+    "ValueSongCount": "{0} sange",
+    "ValueOneMusicVideo": "1 musikvideo",
+    "ValueMusicVideoCount": "{0} musikvideoer",
     "HeaderOffline": "Offline",
-    "HeaderUnaired": "Unaired",
-    "HeaderMissing": "Missing",
-    "ButtonWebsite": "Website",
-    "TooltipFavorite": "Favorite",
+    "HeaderUnaired": "Ikke sendt",
+    "HeaderMissing": "Mangler",
+    "ButtonWebsite": "Hjemmeside",
+    "TooltipFavorite": "Favorit",
     "TooltipLike": "Like",
     "TooltipDislike": "Dislike",
-    "TooltipPlayed": "Played",
-    "ValueSeriesYearToPresent": "{0}-Present",
-    "ValueAwards": "Awards: {0}",
+    "TooltipPlayed": "Afspillet",
+    "ValueSeriesYearToPresent": "{0}-Nu",
+    "ValueAwards": "Priser: {0}",
     "ValueBudget": "Budget: {0}",
-    "ValueRevenue": "Revenue: {0}",
-    "ValuePremiered": "Premiered {0}",
-    "ValuePremieres": "Premieres {0}",
-    "ValueStudio": "Studio: {0}",
-    "ValueStudios": "Studios: {0}",
+    "ValueRevenue": "Indtjening: {0}",
+    "ValuePremiered": "Pr\u00e6miere {0}",
+    "ValuePremieres": "Pr\u00e6miere {0}",
+    "ValueStudio": "Studie: {0}",
+    "ValueStudios": "Studier: {0}",
     "ValueStatus": "Status: {0}",
     "ValueSpecialEpisodeName": "Special - {0}",
-    "LabelLimit": "Limit:",
+    "LabelLimit": "Gr\u00e6nse:",
     "ValueLinks": "Links: {0}",
-    "HeaderPeople": "People",
-    "HeaderCastAndCrew": "Cast & Crew",
-    "ValueArtist": "Artist: {0}",
-    "ValueArtists": "Artists: {0}",
+    "HeaderPeople": "Mennesker",
+    "HeaderCastAndCrew": "Medvirkende",
+    "ValueArtist": "Kunstner: {0}",
+    "ValueArtists": "Kunstnere: {0}",
     "HeaderTags": "Tags",
-    "MediaInfoCameraMake": "Camera make",
-    "MediaInfoCameraModel": "Camera model",
-    "MediaInfoAltitude": "Altitude",
-    "MediaInfoAperture": "Aperture",
-    "MediaInfoExposureTime": "Exposure time",
-    "MediaInfoFocalLength": "Focal length",
-    "MediaInfoOrientation": "Orientation",
-    "MediaInfoIsoSpeedRating": "Iso speed rating",
-    "MediaInfoLatitude": "Latitude",
-    "MediaInfoLongitude": "Longitude",
-    "MediaInfoShutterSpeed": "Shutter speed",
+    "MediaInfoCameraMake": "Kameram\u00e6rke",
+    "MediaInfoCameraModel": "Kameramodel",
+    "MediaInfoAltitude": "H\u00f8jde",
+    "MediaInfoAperture": "Bl\u00e6nde",
+    "MediaInfoExposureTime": "Eksponering",
+    "MediaInfoFocalLength": "Br\u00e6ndvidde",
+    "MediaInfoOrientation": "Orientering",
+    "MediaInfoIsoSpeedRating": "Iso hastigheds rating",
+    "MediaInfoLatitude": "Breddegrad",
+    "MediaInfoLongitude": "H\u00f8jdegrad",
+    "MediaInfoShutterSpeed": "Lukkehastighed",
     "MediaInfoSoftware": "Software",
-    "HeaderIfYouLikeCheckTheseOut": "If you like {0}, check these out...",
-    "HeaderPlotKeywords": "Plot Keywords",
-    "HeaderMovies": "Movies",
+    "HeaderIfYouLikeCheckTheseOut": "Hvis du kan lide {0}, s\u00e5 tjek disse...",
+    "HeaderPlotKeywords": "Plot n\u00f8gleord",
+    "HeaderMovies": "Film",
     "HeaderAlbums": "Albums",
-    "HeaderGames": "Games",
-    "HeaderBooks": "Books",
+    "HeaderGames": "Spil",
+    "HeaderBooks": "B\u00f8ger",
     "HeaderEpisodes": "Afsnit",
-    "HeaderSeasons": "Seasons",
-    "HeaderTracks": "Tracks",
-    "HeaderItems": "Items",
-    "HeaderOtherItems": "Other Items",
-    "ButtonFullReview": "Full review",
-    "ValueAsRole": "as {0}",
-    "ValueGuestStar": "Guest star",
-    "MediaInfoSize": "Size",
-    "MediaInfoPath": "Path",
+    "HeaderSeasons": "S\u00e6soner",
+    "HeaderTracks": "Spor",
+    "HeaderItems": "Element",
+    "HeaderOtherItems": "Andre elementer",
+    "ButtonFullReview": "Fuld anmeldelse",
+    "ValueAsRole": "som {0}",
+    "ValueGuestStar": "G\u00e6stestjerne",
+    "MediaInfoSize": "St\u00f8rrelse",
+    "MediaInfoPath": "Sti",
     "MediaInfoFormat": "Format",
-    "MediaInfoContainer": "Container",
-    "MediaInfoDefault": "Default",
-    "MediaInfoForced": "Forced",
-    "MediaInfoExternal": "External",
-    "MediaInfoTimestamp": "Timestamp",
-    "MediaInfoPixelFormat": "Pixel format",
-    "MediaInfoBitDepth": "Bit depth",
+    "MediaInfoContainer": "Beholder",
+    "MediaInfoDefault": "Standard",
+    "MediaInfoForced": "Tvungen",
+    "MediaInfoExternal": "Ekstern",
+    "MediaInfoTimestamp": "Tidsstempel",
+    "MediaInfoPixelFormat": "Pixelformat",
+    "MediaInfoBitDepth": "Bit dybde",
     "MediaInfoSampleRate": "Sample rate",
     "MediaInfoBitrate": "Bitrate",
-    "MediaInfoChannels": "Channels",
+    "MediaInfoChannels": "Kanaler",
     "MediaInfoLayout": "Layout",
-    "MediaInfoLanguage": "Language",
+    "MediaInfoLanguage": "Sprog",
     "MediaInfoCodec": "Codec",
-    "MediaInfoProfile": "Profile",
-    "MediaInfoLevel": "Level",
-    "MediaInfoAspectRatio": "Aspect ratio",
-    "MediaInfoResolution": "Resolution",
-    "MediaInfoAnamorphic": "Anamorphic",
+    "MediaInfoProfile": "Profil",
+    "MediaInfoLevel": "Niveau",
+    "MediaInfoAspectRatio": "Formatforhold",
+    "MediaInfoResolution": "Opl\u00f8sning",
+    "MediaInfoAnamorphic": "Anamorfisk",
     "MediaInfoInterlaced": "Interlaced",
     "MediaInfoFramerate": "Framerate",
-    "MediaInfoStreamTypeAudio": "Audio",
+    "MediaInfoStreamTypeAudio": "Lyd",
     "MediaInfoStreamTypeData": "Data",
     "MediaInfoStreamTypeVideo": "Video",
-    "MediaInfoStreamTypeSubtitle": "Subtitle",
-    "MediaInfoStreamTypeEmbeddedImage": "Embedded Image",
+    "MediaInfoStreamTypeSubtitle": "Undertekster",
+    "MediaInfoStreamTypeEmbeddedImage": "Indlejret billede",
     "MediaInfoRefFrames": "Ref frames",
-    "TabPlayback": "Playback",
-    "TabNotifications": "Notifikationer",
-    "TabExpert": "Expert",
-    "HeaderSelectCustomIntrosPath": "Select Custom Intros Path",
-    "HeaderRateAndReview": "Rate and Review",
-    "HeaderThankYou": "Thank You",
-    "MessageThankYouForYourReview": "Thank you for your review.",
-    "LabelYourRating": "Your rating:",
-    "LabelFullReview": "Full review:",
-    "LabelShortRatingDescription": "Short rating summary:",
-    "OptionIRecommendThisItem": "I recommend this item",
-    "WebClientTourContent": "View your recently added media, next episodes, and more. The green circles indicate how many unplayed items you have.",
-    "WebClientTourMovies": "Play movies, trailers and more from any device with a web browser",
-    "WebClientTourMouseOver": "Hold the mouse over any poster for quick access to important information",
-    "WebClientTourTapHold": "Tap and hold or right click any poster for a context menu",
-    "WebClientTourMetadataManager": "Click edit to open the metadata manager",
-    "WebClientTourPlaylists": "Easily create playlists and instant mixes, and play them on any device",
-    "WebClientTourCollections": "Create movie collections to group box sets together",
-    "WebClientTourUserPreferences1": "User preferences allow you to customize the way your library is presented in all of your Emby apps",
-    "WebClientTourUserPreferences2": "Configure your audio and subtitle language settings once, for every Emby app",
-    "WebClientTourUserPreferences3": "Design the web client home page to your liking",
-    "WebClientTourUserPreferences4": "Configure backdrops, theme songs and external players",
-    "WebClientTourMobile1": "The web client works great on smartphones and tablets...",
-    "WebClientTourMobile2": "and easily controls other devices and Emby apps",
-    "WebClientTourMySync": "Sync your personal media to your devices for offline viewing.",
-    "MessageEnjoyYourStay": "Enjoy your stay",
-    "DashboardTourDashboard": "The server dashboard allows you to monitor your server and your users. You'll always know who is doing what and where they are.",
-    "DashboardTourHelp": "In-app help provides easy buttons to open wiki pages relating to the on-screen content.",
-    "DashboardTourUsers": "Easily create user accounts for your friends and family, each with their own permissions, library access, parental controls and more.",
-    "DashboardTourCinemaMode": "Cinema mode brings the theater experience straight to your living room with the ability to play trailers and custom intros before the main feature.",
-    "DashboardTourChapters": "Enable chapter image generation for your videos for a more pleasing presentation while viewing.",
-    "DashboardTourSubtitles": "Automatically download subtitles for your videos in any language.",
-    "DashboardTourPlugins": "Install plugins such as internet video channels, live tv, metadata scanners, and more.",
-    "DashboardTourNotifications": "Automatically send notifications of server events to your mobile device, email and more.",
-    "DashboardTourScheduledTasks": "Easily manage long running operations with scheduled tasks. Decide when they run, and how often.",
-    "DashboardTourMobile": "The Emby Server dashboard works great on smartphones and tablets. Manage your server from the palm of your hand anytime, anywhere.",
-    "DashboardTourSync": "Sync your personal media to your devices for offline viewing.",
-    "MessageRefreshQueued": "Refresh queued",
-    "TabDevices": "Devices",
-    "TabExtras": "Extras",
-    "DeviceLastUsedByUserName": "Last used by {0}",
-    "HeaderDeleteDevice": "Delete Device",
-    "DeleteDeviceConfirmation": "Are you sure you wish to delete this device? It will reappear the next time a user signs in with it.",
-    "LabelEnableCameraUploadFor": "Enable camera upload for:",
-    "HeaderSelectUploadPath": "Select Upload Path",
-    "LabelEnableCameraUploadForHelp": "Uploads will occur automatically in the background when signed into Emby.",
-    "ErrorMessageStartHourGreaterThanEnd": "End time must be greater than the start time.",
-    "ButtonLibraryAccess": "Library access",
-    "ButtonParentalControl": "Parental control",
-    "HeaderInvitationSent": "Invitation Sent",
-    "MessageInvitationSentToUser": "An email has been sent to {0}, inviting them to accept your sharing invitation.",
-    "MessageInvitationSentToNewUser": "An email has been sent to {0} inviting them to sign up with Emby.",
-    "HeaderConnectionFailure": "Connection Failure",
-    "MessageUnableToConnectToServer": "We're unable to connect to the selected server right now. Please ensure it is running and try again.",
-    "ButtonSelectServer": "Select server",
-    "MessagePluginConfigurationRequiresLocalAccess": "To configure this plugin please sign in to your local server directly.",
-    "MessageLoggedOutParentalControl": "Access is currently restricted. Please try again later.",
-    "DefaultErrorMessage": "There was an error processing the request. Please try again later.",
-    "ButtonAccept": "Accept",
-    "ButtonReject": "Reject",
-    "HeaderForgotPassword": "Forgot Password",
-    "MessageContactAdminToResetPassword": "Please contact your system administrator to reset your password.",
-    "MessageForgotPasswordInNetworkRequired": "Please try again within your home network to initiate the password reset process.",
-    "MessageForgotPasswordFileCreated": "The following file has been created on your server and contains instructions on how to proceed:",
-    "MessageForgotPasswordFileExpiration": "The reset pin will expire at {0}.",
-    "MessageInvalidForgotPasswordPin": "An invalid or expired pin was entered. Please try again.",
-    "MessagePasswordResetForUsers": "Passwords have been removed for the following users:",
-    "HeaderInviteGuest": "Invite Guest",
-    "ButtonLinkMyEmbyAccount": "Link my account now",
-    "MessageConnectAccountRequiredToInviteGuest": "In order to invite guests you need to first link your Emby account to this server.",
+    "TabPlayback": "Afspilning",
+    "TabNotifications": "Underretninger",
+    "TabExpert": "Ekspert",
+    "HeaderSelectCustomIntrosPath": "V\u00e6lg sti til brugerdefinerede introduktioner",
+    "HeaderRateAndReview": "Bed\u00f8m og anmeld",
+    "HeaderThankYou": "Tak",
+    "MessageThankYouForYourReview": "Tak for din anmeldelse",
+    "LabelYourRating": "Din bed\u00f8mmelse:",
+    "LabelFullReview": "Fuld anmeldelse:",
+    "LabelShortRatingDescription": "Kort bed\u00f8mmelsesresum\u00e9:",
+    "OptionIRecommendThisItem": "Jeg anbefaler dette",
+    "WebClientTourContent": "Se dit seneste tilf\u00f8jet media, kommende episoder samt mere. Den gr\u00f8nne cirkel indikerer hvor mange uafspillet elementer du har.",
+    "WebClientTourMovies": "Afspil film, trailere samt andet fra hvilken som helst enhed med en browser",
+    "WebClientTourMouseOver": "Hold musen over enhver plakat for hurtig adgang til vigtig information",
+    "WebClientTourTapHold": "Tryk og hold eller h\u00f8jreklik p\u00e5 enhver plakat for at \u00e5bne en menu for det valgte element",
+    "WebClientTourMetadataManager": "Klik p\u00e5 rediger for at \u00e5bne metadata manageren",
+    "WebClientTourPlaylists": "Opret afspilningslister og instant mixes, og afspil dem p\u00e5 enhver enhed",
+    "WebClientTourCollections": "Opret filmsamlinger s\u00e5 film kan grupperes sammen",
+    "WebClientTourUserPreferences1": "Brugerindstillinger g\u00f8r det muligt for dig at skr\u00e6ddersy m\u00e5den dit bibliotek pr\u00e6senteres i alle dine Emby apps",
+    "WebClientTourUserPreferences2": "Konfigurer sproget p\u00e5 dine lyd og undertekstindstillinger \u00e9n gang for alle Emby apps",
+    "WebClientTourUserPreferences3": "Design webklient hjemmesiden til din egen smag",
+    "WebClientTourUserPreferences4": "V\u00e6lg baggrunde, temasange og eksterne afspillere",
+    "WebClientTourMobile1": "Webklienten virker perfekt p\u00e5 smartphones og tablets...",
+    "WebClientTourMobile2": "og styr let andre enheder og Emby apps",
+    "WebClientTourMySync": "Synkroniser dine personlige mediefiler til dine enheder s\u00e5 det kan ses offline.",
+    "MessageEnjoyYourStay": "Nyd dit bes\u00f8g",
+    "DashboardTourDashboard": "Betjeningspanelet g\u00f8r det muligt at monitorere din server og dine brugere. Du vil altid v\u00e6re i stand til at vide hvem der g\u00f8r hvad samt hvor de er.",
+    "DashboardTourHelp": "Hj\u00e6lp inde i app'en s\u00f8rger for knapper der let \u00e5bner de wiki-sider der er relateret til hvad der er p\u00e5 din sk\u00e6rm i det \u00f8jeblik.",
+    "DashboardTourUsers": "Opret let brugerkonti til dine venner og familie, hver med deres egne rettigheder, adgang til biblioteket, for\u00e6ldre-indstillinger samt meget mere.",
+    "DashboardTourCinemaMode": "Biograftilstand giver dig biografoplevelsen direkte ind i din stue, med muligheden for at vise trailere og brugerdefinerede introduktioner f\u00f8r hovedfilmen.",
+    "DashboardTourChapters": "Aktiver kapitelbillede-oprettelse for dine videoer for en mere behagelig pr\u00e6sentation mens du afspiller.",
+    "DashboardTourSubtitles": "Download automatisk undertekster til dine videoer in ethvert sprog.",
+    "DashboardTourPlugins": "Installer tilf\u00f8jelser s\u00e5 som internet videokanaler, live tv, metadata skannere samt meget mere.",
+    "DashboardTourNotifications": "Send automatisk notifikationer vedr\u00f8rende serverbegivenheder til dine mobile enheder, din email samt andre tjenester.",
+    "DashboardTourScheduledTasks": "Administrer let processer der l\u00f8ber over l\u00e6ngere tid via planlagte opgaver. Bestem hvorn\u00e5r de udf\u00f8res samt hvor ofte.",
+    "DashboardTourMobile": "Emby betjeningspanelet virker uden problemer p\u00e5 b\u00e5de smartphones og tablets. Kontrol over din server er altid ved dine fingrespidser hvor som helst, n\u00e5r som helst.",
+    "DashboardTourSync": "Synkroniser dine personlige mediefiler til dine enheder s\u00e5 det kan ses offline.",
+    "MessageRefreshQueued": "Opdatering sat i k\u00f8",
+    "TabDevices": "Enheder",
+    "TabExtras": "Ekstra",
+    "DeviceLastUsedByUserName": "Sidst brugt af {0}",
+    "HeaderDeleteDevice": "Slet enhed",
+    "DeleteDeviceConfirmation": "Er du sikker p\u00e5 du \u00f8nsker at slette denne enhed? Den vil dukke op igen n\u00e6ste gang en bruger logger ind med den.",
+    "LabelEnableCameraUploadFor": "Aktiver kamera upload for:",
+    "HeaderSelectUploadPath": "V\u00e6lg upload sti",
+    "LabelEnableCameraUploadForHelp": "Uploads sker automatisk i baggrunden n\u00e5r du er logget p\u00e5 Emby",
+    "ErrorMessageStartHourGreaterThanEnd": "Slut tid skal v\u00e6re st\u00f8rre end start tid.",
+    "ButtonLibraryAccess": "Biblioteksadgang",
+    "ButtonParentalControl": "For\u00e6ldrekontrol",
+    "HeaderInvitationSent": "Invitation sendt",
+    "MessageInvitationSentToUser": "En email er blevet sendt til {0}, hvori de er blevet anmodet om at acceptere din invitation.",
+    "MessageInvitationSentToNewUser": "En email er blevet sendt til {0} med en invitation til at oprette sig hos Emby.",
+    "HeaderConnectionFailure": "Forbindelsesfejl",
+    "MessageUnableToConnectToServer": "Vi kan ikke forbinde til den valgte server p\u00e5 nuv\u00e6rende tidspunkt. Sikrer dig venligst at serveren k\u00f8rer og pr\u00f8v igen.",
+    "ButtonSelectServer": "V\u00e6lg server",
+    "MessagePluginConfigurationRequiresLocalAccess": "For at konfigurerer dette plugin log da venligst direkte ind p\u00e5 din lokale server.",
+    "MessageLoggedOutParentalControl": "Adgang er begr\u00e6nset p\u00e5 nuv\u00e6rende tidspunkt. Pr\u00f8v igen senere.",
+    "DefaultErrorMessage": "Det opstod en fejl ved behandlingen af foresp\u00f8rgslen. Pr\u00f8v igen senere.",
+    "ButtonAccept": "Accepter",
+    "ButtonReject": "Afvis",
+    "HeaderForgotPassword": "Glemt adgangskode",
+    "MessageContactAdminToResetPassword": "Kontakt venligst din systemadministrator for at nulstille din adgangskode.",
+    "MessageForgotPasswordInNetworkRequired": "Pr\u00f8v igen inde i dit hjemmenetv\u00e6rk for at igangs\u00e6tte nulstilling af din adgangskode.",
+    "MessageForgotPasswordFileCreated": "Den f\u00f8lgende fil er blevet oprettet p\u00e5 din server og indeholder instruktioner vedr\u00f8rende hvordan du skal forts\u00e6tte:",
+    "MessageForgotPasswordFileExpiration": "Nulstillings pinkoden udl\u00f8ber {0}.",
+    "MessageInvalidForgotPasswordPin": "En ugyldig eller udl\u00f8bet pinkode blev indtastet. Pr\u00f8v igen.",
+    "MessagePasswordResetForUsers": "Adgangskoder er blevet fjernet fra f\u00f8lgende brugere:",
+    "HeaderInviteGuest": "Inviter g\u00e6st",
+    "ButtonLinkMyEmbyAccount": "Link min konto nu",
+    "MessageConnectAccountRequiredToInviteGuest": "For at invitere g\u00e6ster skal du f\u00f8rst k\u00e6de din Emby konto til denne server.",
     "ButtonSync": "Sync",
-    "SyncMedia": "Sync Media",
-    "HeaderCancelSyncJob": "Cancel Sync",
-    "CancelSyncJobConfirmation": "Cancelling the sync job will remove synced media from the device during the next sync process. Are you sure you wish to proceed?",
+    "SyncMedia": "Synkroniser medier",
+    "HeaderCancelSyncJob": "Afbryd synkronisering",
+    "CancelSyncJobConfirmation": "Afbrydelse af synkroniseringen vil fjerne medier fra enheden under n\u00e6ste synkroniseringsproces. Er du sikker p\u00e5 du \u00f8nsker at forts\u00e6tte?",
     "TabSync": "Sync",
-    "MessagePleaseSelectDeviceToSyncTo": "Please select a device to sync to.",
-    "MessageSyncJobCreated": "Sync job created.",
-    "LabelSyncTo": "Sync to:",
-    "LabelSyncJobName": "Sync job name:",
-    "LabelQuality": "Quality:",
-    "HeaderSettings": "Settings",
-    "OptionAutomaticallySyncNewContent": "Automatically sync new content",
-    "OptionAutomaticallySyncNewContentHelp": "New content added to this category will be automatically synced to the device.",
-    "OptionSyncUnwatchedVideosOnly": "Sync unwatched videos only",
-    "OptionSyncUnwatchedVideosOnlyHelp": "Only unwatched videos will be synced, and videos will be removed from the device as they are watched.",
-    "LabelItemLimit": "Item limit:",
-    "LabelItemLimitHelp": "Optional. Set a limit to the number of items that will be synced.",
-    "MessageBookPluginRequired": "Requires installation of the Bookshelf plugin",
-    "MessageGamePluginRequired": "Requires installation of the GameBrowser plugin",
-    "MessageUnsetContentHelp": "Content will be displayed as plain folders. For best results use the metadata manager to set the content types of sub-folders.",
-    "SyncJobItemStatusQueued": "Queued",
-    "SyncJobItemStatusConverting": "Converting",
-    "SyncJobItemStatusTransferring": "Transferring",
-    "SyncJobItemStatusSynced": "Synced",
-    "SyncJobItemStatusFailed": "Failed",
-    "SyncJobItemStatusRemovedFromDevice": "Removed from device",
-    "SyncJobItemStatusCancelled": "Cancelled",
-    "LabelProfile": "Profile:",
+    "MessagePleaseSelectDeviceToSyncTo": "V\u00e6lg en enhed at synkroniserer til.",
+    "MessageSyncJobCreated": "Synkroniserings job oprettet",
+    "LabelSyncTo": "Synkroniser til:",
+    "LabelSyncJobName": "Navn til synkroniserings job:",
+    "LabelQuality": "Kvalitet:",
+    "HeaderSettings": "Indstillinger",
+    "OptionAutomaticallySyncNewContent": "Synkroniser automatisk nyt indhold",
+    "OptionAutomaticallySyncNewContentHelp": "Nyt indhold i denne kategori vil automatisk blive synkroniseret til enheden.",
+    "OptionSyncUnwatchedVideosOnly": "Synkroniser kun usete videoer",
+    "OptionSyncUnwatchedVideosOnlyHelp": "Kun usete videoer vil blive synkroniseret, og videoer vil blive fjernet fra enheden n\u00e5r de er blevet set.",
+    "LabelItemLimit": "Maks. filer:",
+    "LabelItemLimitHelp": "Valgfri. S\u00e6t en gr\u00e6nse for antallet af filer der synkroniseres.",
+    "MessageBookPluginRequired": "Kr\u00e6ver installation af Bookshelf tilf\u00f8jelsen",
+    "MessageGamePluginRequired": "Kr\u00e6ver installation af GameBrowser tilf\u00f8jelsen",
+    "MessageUnsetContentHelp": "Indhold vil blive vist som almindelige mapper. For det bedste resultat benyt metadata manageren til at v\u00e6lge indholdstypen i undermapper.",
+    "SyncJobItemStatusQueued": "Sat i k\u00f8",
+    "SyncJobItemStatusConverting": "Konverterer",
+    "SyncJobItemStatusTransferring": "Overf\u00f8rer",
+    "SyncJobItemStatusSynced": "Synkroniseret",
+    "SyncJobItemStatusFailed": "Fejlet",
+    "SyncJobItemStatusRemovedFromDevice": "Fjernet fra enhed",
+    "SyncJobItemStatusCancelled": "Annulleret",
+    "LabelProfile": "Profil:",
     "LabelBitrateMbps": "Bitrate (Mbps):",
-    "EmbyIntroDownloadMessage": "To download and install Emby Server visit {0}.",
-    "ButtonNewServer": "New Server",
-    "ButtonSignInWithConnect": "Sign in with Emby Connect",
-    "HeaderNewServer": "New Server",
-    "MyDevice": "My Device",
-    "ButtonRemote": "Remote"
+    "EmbyIntroDownloadMessage": "For at downloade og installere Emby bes\u00f8g {0}.",
+    "ButtonNewServer": "Ny server",
+    "ButtonSignInWithConnect": "Log ind med Emby Connect",
+    "HeaderNewServer": "Ny server",
+    "MyDevice": "Min enhed",
+    "ButtonRemote": "Fjernbetjening",
+    "TabInfo": "Info",
+    "TabCast": "Cast",
+    "TabScenes": "Scener"
 }

+ 16 - 12
MediaBrowser.Server.Implementations/Localization/JavaScript/de.json

@@ -40,13 +40,14 @@
     "TitleLiveTV": "Live-TV",
     "TitleSync": "Synchronisation",
     "ButtonDonate": "Spenden",
+    "LabelRecurringDonationCanBeCancelledHelp": "Fortlaufende Spenden k\u00f6nnen jederzeit \u00fcber deinen PayPal Account gek\u00fcndigt werden.",
     "HeaderMyMedia": "Meine Medien",
     "TitleNotifications": "Benachrichtigungen",
     "ErrorLaunchingChromecast": "W\u00e4hrend des startens von Chromecast ist ein Fehler aufgetreten. Bitte stelle sicher, dass dein Ger\u00e4te mit dem WLAN verbunden ist.",
     "MessageErrorLoadingSupporterInfo": "Es trat ein Fehler beim laden der Unterst\u00fctzer-Informationen auf. Bitte versuchen Sie es sp\u00e4ter erneut.",
     "MessageLinkYourSupporterKey": "Verbinden Sie Ihren Unterst\u00fctzer-Schl\u00fcssel mit bis zu {0} Emby Connect Benutzern um kostenfreien Zugriff auf die folgenden Apps zu erhalten:",
     "HeaderConfirmRemoveUser": "Entferne Benutzer",
-    "MessageSwipeDownOnRemoteControl": "Welcome to remote control. Select the device to control by clicking the cast icon in the upper right corner. Swipe down anywhere on this screen to go back to where you came from.",
+    "MessageSwipeDownOnRemoteControl": "Willkommen zur Fernbedienung. W\u00e4hlen Sie ein Ger\u00e4t durch Klick auf das Cast-Icon in der rechten oberen Ecke, um es fernzusteuern. Streichen Sie irgendwo auf dem Bildschirm nach unten um zur\u00fcck zu gehen.",
     "MessageConfirmRemoveConnectSupporter": "M\u00f6chten Sie wirklich zus\u00e4tzliche Unterst\u00fctzer-Features von diesem Anwender entfernen?",
     "ValueTimeLimitSingleHour": "Zeitlimit: 1 Stunde",
     "ValueTimeLimitMultiHour": "Zeitlimit: {0} Stunden",
@@ -97,7 +98,7 @@
     "HeaderSupporterBenefit": "Eine Unterst\u00fctzer-Mitgliedschaft bietet weitere Funktionen wie z.B. Zugriff auf die Synchronisation, Premium-Plugins, Internet Kan\u00e4le und mehr. {0}Erfahren Sie mehr{1}.",
     "LabelSyncNoTargetsHelp": "Es sieht so aus als w\u00fcrden Sie aktuell keine Apps verwenden, die Synchronisation unterst\u00fctzen.",
     "HeaderWelcomeToProjectServerDashboard": "Willkommen zur Emby Server \u00dcbersicht",
-    "HeaderWelcomeToProjectWebClient": "Willkommen im Emby Web-Client",
+    "HeaderWelcomeToProjectWebClient": "Willkommen zu Emby",
     "ButtonTakeTheTour": "Mache die Tour",
     "HeaderWelcomeBack": "Willkommen zur\u00fcck!",
     "TitlePlugins": "Plugins",
@@ -123,7 +124,7 @@
     "LabelFree": "Frei",
     "HeaderPlaybackError": "Wiedergabefehler",
     "MessagePlaybackErrorNotAllowed": "Sie sind nicht befugt diese Inhalte wiederzugeben. Bitte kontaktieren Sie Ihren Systemadministrator f\u00fcr weitere Details.",
-    "MessagePlaybackErrorNoCompatibleStream": "No compatible streams are currently available. Please try again later or contact your system administrator for details.",
+    "MessagePlaybackErrorNoCompatibleStream": "Es sind keine kompatiblen Streams verf\u00fcgbar. Bitte versuchen Sie es sp\u00e4ter erneut oder kontaktieren Sie Ihren Systemadministrator f\u00fcr weitere Details.",
     "MessagePlaybackErrorRateLimitExceeded": "Ihr Wiedergabelimit wurde \u00fcberschritten. Bitte kontaktieren Sie Ihren Systemadministrator f\u00fcr weitere Details.",
     "MessagePlaybackErrorPlaceHolder": "Der gew\u00e4hlte Inhalt kann auf diesem Ger\u00e4t nicht abgespielt werden.",
     "HeaderSelectAudio": "W\u00e4hle Audio",
@@ -398,7 +399,7 @@
     "TabMetadata": "Metadata",
     "TabDLNA": "DLNA",
     "TabLiveTV": "Live-TV",
-    "TabAutoOrganize": "Automatische Organisation",
+    "TabAutoOrganize": "Autom.Organisation",
     "TabPlugins": "Plugins",
     "TabAdvanced": "Erweitert",
     "TabHelp": "Hilfe",
@@ -586,7 +587,7 @@
     "TooltipLike": "Like",
     "TooltipDislike": "Dislike",
     "TooltipPlayed": "Gespielt",
-    "ValueSeriesYearToPresent": "{0}-vorhanden",
+    "ValueSeriesYearToPresent": "{0}-heute",
     "ValueAwards": "Auszeichnungen: {0}",
     "ValueBudget": "Budget: {0}",
     "ValueRevenue": "Einnahmen: {0}",
@@ -688,7 +689,7 @@
     "DashboardTourHelp": "Die In-App-Hilfe Schaltfl\u00e4che bietet eine schnelle M\u00f6glichkeit um eine Wiki-Seite zum aktuellen Inhalt zu \u00f6ffnen.",
     "DashboardTourUsers": "Erstelle einfach Benutzeraccounts f\u00fcr Freunde und Familie. Jeder mit seinen individuellen Einstellungen bei Berechtigungen, Blibliothekenzugriff, Kindersicherung und mehr.",
     "DashboardTourCinemaMode": "Der Kino-Modus bringt das Kinoerlebnis direkt in dein Wohnzimmer, mit der F\u00e4higkeit Trailer und benutzerdefinierte Intros vor dem Hauptfilm zu spielen.",
-    "DashboardTourChapters": "Aktiviere die Bildgenerierung f\u00fcr die Kapitel deiner Videos f\u00fcr eine bessere Darstellung w\u00e4hrend des Ansehens.",
+    "DashboardTourChapters": "Aktiviere Kapitel-Bilder Generierung f\u00fcr Videos f\u00fcr eine bessere Darstellung.",
     "DashboardTourSubtitles": "Lade automatisch Untertitel f\u00fcr jede Sprache f\u00fcr deine Videos herunter.",
     "DashboardTourPlugins": "Installiere Plugins wie Internet Videoportale, Live-TV, Metadatenscanner und mehr.",
     "DashboardTourNotifications": "Sende automatisch Benachrichtigungen von Serverereignissen auf dein mobiles Endger\u00e4t, per E-Mail und mehr.",
@@ -757,10 +758,13 @@
     "SyncJobItemStatusCancelled": "Abgebrochen",
     "LabelProfile": "Profil:",
     "LabelBitrateMbps": "Datenrate (Mbps):",
-    "EmbyIntroDownloadMessage": "To download and install Emby Server visit {0}.",
-    "ButtonNewServer": "New Server",
-    "ButtonSignInWithConnect": "Sign in with Emby Connect",
-    "HeaderNewServer": "New Server",
-    "MyDevice": "My Device",
-    "ButtonRemote": "Remote"
+    "EmbyIntroDownloadMessage": "Um Emby herunterzuladen und zu installieren, besuchen Sie: {0}.",
+    "ButtonNewServer": "Neuer Server",
+    "ButtonSignInWithConnect": "Anmelden mit Emby Connect",
+    "HeaderNewServer": "Neuer Server",
+    "MyDevice": "Mein Ger\u00e4t",
+    "ButtonRemote": "Fernbedienung",
+    "TabInfo": "Info",
+    "TabCast": "Darsteller",
+    "TabScenes": "Szenen"
 }

+ 6 - 2
MediaBrowser.Server.Implementations/Localization/JavaScript/el.json

@@ -40,6 +40,7 @@
     "TitleLiveTV": "Live TV",
     "TitleSync": "Sync",
     "ButtonDonate": "Donate",
+    "LabelRecurringDonationCanBeCancelledHelp": "Recurring donations can be cancelled at any time from within your PayPal account.",
     "HeaderMyMedia": "My Media",
     "TitleNotifications": "\u0395\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2",
     "ErrorLaunchingChromecast": "There was an error launching chromecast. Please ensure your device is connected to your wireless network.",
@@ -97,7 +98,7 @@
     "HeaderSupporterBenefit": "A supporter membership provides additional benefits such as access to sync, premium plugins, internet channel content, and more. {0}Learn more{1}.",
     "LabelSyncNoTargetsHelp": "It looks like you don't currently have any apps that support sync.",
     "HeaderWelcomeToProjectServerDashboard": "Welcome to the Emby Server Dashboard",
-    "HeaderWelcomeToProjectWebClient": "Welcome to the Emby Web Client",
+    "HeaderWelcomeToProjectWebClient": "Welcome to Emby",
     "ButtonTakeTheTour": "\u039a\u03ac\u03bd\u03c4\u03b5 \u03c4\u03b7\u03bd \u039e\u03b5\u03bd\u03ac\u03b3\u03b7\u03c3\u03b7",
     "HeaderWelcomeBack": "Welcome back!",
     "TitlePlugins": "\u03a0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03b1",
@@ -762,5 +763,8 @@
     "ButtonSignInWithConnect": "Sign in with Emby Connect",
     "HeaderNewServer": "New Server",
     "MyDevice": "My Device",
-    "ButtonRemote": "Remote"
+    "ButtonRemote": "Remote",
+    "TabInfo": "Info",
+    "TabCast": "Cast",
+    "TabScenes": "Scenes"
 }

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است