Browse Source

Merge branch 'master' of https://github.com/MediaBrowser/MediaBrowser

simon 11 years ago
parent
commit
37aab84b2a
49 changed files with 655 additions and 196 deletions
  1. 3 8
      MediaBrowser.Api/BaseApiService.cs
  2. 0 70
      MediaBrowser.Api/Library/LibraryHelpers.cs
  3. 32 0
      MediaBrowser.Api/LiveTv/LiveTvService.cs
  4. 3 2
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  5. 1 1
      MediaBrowser.Api/Playback/Hls/AudioHlsService.cs
  6. 18 11
      MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
  7. 151 1
      MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
  8. 1 1
      MediaBrowser.Api/Playback/Progressive/AudioService.cs
  9. 1 1
      MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
  10. 1 1
      MediaBrowser.Api/Playback/Progressive/VideoService.cs
  11. 6 2
      MediaBrowser.Api/Playback/StreamState.cs
  12. 0 2
      MediaBrowser.Common/MediaBrowser.Common.csproj
  13. 1 1
      MediaBrowser.Controller/Entities/Audio/Audio.cs
  14. 10 2
      MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
  15. 18 7
      MediaBrowser.Controller/LiveTv/ILiveTvService.cs
  16. 21 0
      MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
  17. 2 0
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  18. 1 2
      MediaBrowser.Controller/MediaInfo/FFMpegManager.cs
  19. 6 9
      MediaBrowser.Controller/MediaInfo/IMediaEncoder.cs
  20. 4 4
      MediaBrowser.Controller/MediaInfo/InternalMediaInfoResult.cs
  21. 15 6
      MediaBrowser.Controller/MediaInfo/MediaEncoderHelpers.cs
  22. 11 1
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  23. 3 1
      MediaBrowser.Model/Dto/BaseItemDto.cs
  24. 2 0
      MediaBrowser.Model/Dto/BaseItemPerson.cs
  25. 2 0
      MediaBrowser.Model/Dto/ChapterInfoDto.cs
  26. 2 0
      MediaBrowser.Model/Dto/StudioDto.cs
  27. 2 0
      MediaBrowser.Model/Dto/UserDto.cs
  28. 2 0
      MediaBrowser.Model/Entities/BaseItemInfo.cs
  29. 24 1
      MediaBrowser.Model/Entities/MediaStream.cs
  30. 27 0
      MediaBrowser.Model/LiveTv/ProgramQuery.cs
  31. 3 1
      MediaBrowser.Model/Session/SessionInfoDto.cs
  32. 2 2
      MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
  33. 4 5
      MediaBrowser.Providers/MediaInfo/BaseFFProbeProvider.cs
  34. 2 3
      MediaBrowser.Providers/MediaInfo/FFProbeAudioInfoProvider.cs
  35. 3 4
      MediaBrowser.Providers/MediaInfo/FFProbeVideoInfoProvider.cs
  36. 1 2
      MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
  37. 4 12
      MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs
  38. 8 0
      MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs
  39. 1 1
      MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs
  40. 217 9
      MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
  41. 12 12
      MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs
  42. 3 4
      MediaBrowser.ServerApplication/ApplicationHost.cs
  43. 1 0
      MediaBrowser.WebDashboard/Api/DashboardService.cs
  44. 12 1
      MediaBrowser.WebDashboard/ApiClient.js
  45. 6 0
      MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
  46. 1 1
      MediaBrowser.WebDashboard/packages.config
  47. 2 2
      Nuget/MediaBrowser.Common.Internal.nuspec
  48. 1 1
      Nuget/MediaBrowser.Common.nuspec
  49. 2 2
      Nuget/MediaBrowser.Server.Core.nuspec

+ 3 - 8
MediaBrowser.Api/BaseApiService.cs

@@ -1,13 +1,13 @@
-using System.IO;
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Logging;
+using ServiceStack.Web;
 using System;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
-using ServiceStack.Web;
 
 namespace MediaBrowser.Api
 {
@@ -52,11 +52,6 @@ namespace MediaBrowser.Api
             return ResultFactory.GetOptimizedResult(Request, result);
         }
 
-        protected object ToStreamResult(Stream stream, string contentType)
-        {
-            return ResultFactory.GetResult(stream, contentType);
-        }
-
         /// <summary>
         /// To the optimized result using cache.
         /// </summary>

+ 0 - 70
MediaBrowser.Api/Library/LibraryHelpers.cs

@@ -68,8 +68,6 @@ namespace MediaBrowser.Api.Library
             var rootFolderPath = user != null ? user.RootFolderPath : appPaths.DefaultUserViewsPath;
             var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 
-            ValidateNewMediaPath(fileSystem, rootFolderPath, path);
-
             var shortcutFilename = Path.GetFileNameWithoutExtension(path);
 
             var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
@@ -82,73 +80,5 @@ namespace MediaBrowser.Api.Library
 
             fileSystem.CreateShortcut(lnk, path);
         }
-
-        /// <summary>
-        /// Validates that a new media path can be added
-        /// </summary>
-        /// <param name="fileSystem">The file system.</param>
-        /// <param name="currentViewRootFolderPath">The current view root folder path.</param>
-        /// <param name="mediaPath">The media path.</param>
-        /// <exception cref="System.ArgumentException">
-        /// </exception>
-        private static void ValidateNewMediaPath(IFileSystem fileSystem, string currentViewRootFolderPath, string mediaPath)
-        {
-            var pathsInCurrentVIew = Directory.EnumerateFiles(currentViewRootFolderPath, ShortcutFileSearch, SearchOption.AllDirectories)
-                    .Select(fileSystem.ResolveShortcut)
-                    .ToList();
-
-            // Don't allow duplicate sub-paths within the same user library, or it will result in duplicate items
-            // See comments in IsNewPathValid
-            var duplicate = pathsInCurrentVIew
-              .FirstOrDefault(p => !IsNewPathValid(fileSystem, mediaPath, p));
-
-            if (!string.IsNullOrEmpty(duplicate))
-            {
-                throw new ArgumentException(string.Format("The path cannot be added to the library because {0} already exists.", duplicate));
-            }
-            
-            // Make sure the current root folder doesn't already have a shortcut to the same path
-            duplicate = pathsInCurrentVIew
-                .FirstOrDefault(p => string.Equals(mediaPath, p, StringComparison.OrdinalIgnoreCase));
-
-            if (!string.IsNullOrEmpty(duplicate))
-            {
-                throw new ArgumentException(string.Format("The path {0} already exists in the library", mediaPath));
-            }
-        }
-
-        /// <summary>
-        /// Validates that a new path can be added based on an existing path
-        /// </summary>
-        /// <param name="fileSystem">The file system.</param>
-        /// <param name="newPath">The new path.</param>
-        /// <param name="existingPath">The existing path.</param>
-        /// <returns><c>true</c> if [is new path valid] [the specified new path]; otherwise, <c>false</c>.</returns>
-        private static bool IsNewPathValid(IFileSystem fileSystem, string newPath, string existingPath)
-        {
-            // Example: D:\Movies is the existing path
-            // D:\ cannot be added
-            // Neither can D:\Movies\Kids
-            // A D:\Movies duplicate is ok here since that will be caught later
-
-            if (string.Equals(newPath, existingPath, StringComparison.OrdinalIgnoreCase))
-            {
-                return true;
-            }
-
-            // If enforceSubPathRestriction is true, validate the D:\Movies\Kids scenario
-            if (fileSystem.ContainsSubPath(existingPath, newPath))
-            {
-                return false;
-            }
-
-            // Validate the D:\ scenario
-            if (fileSystem.ContainsSubPath(newPath, existingPath))
-            {
-                return false;
-            }
-
-            return true;
-        }
     }
 }

+ 32 - 0
MediaBrowser.Api/LiveTv/LiveTvService.cs

@@ -154,6 +154,23 @@ namespace MediaBrowser.Api.LiveTv
         public string MaxEndDate { get; set; }
     }
 
+    [Route("/LiveTv/Programs/Recommended", "GET")]
+    [Api(Description = "Gets available live tv epgs..")]
+    public class GetRecommendedPrograms : IReturn<QueryResult<ProgramInfoDto>>
+    {
+        [ApiMember(Name = "UserId", Description = "Optional filter by user id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
+        public string UserId { get; set; }
+
+        [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; }
+
+        [ApiMember(Name = "IsAiring", Description = "Optional. Filter by programs that are currently airing, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool? IsAiring { get; set; }
+
+        [ApiMember(Name = "HasAired", Description = "Optional. Filter by programs that have completed airing, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool? HasAired { get; set; }
+    }
+
     [Route("/LiveTv/Programs/{Id}", "GET")]
     [Api(Description = "Gets a live tv program")]
     public class GetProgram : IReturn<ProgramInfoDto>
@@ -331,6 +348,21 @@ namespace MediaBrowser.Api.LiveTv
             return ToOptimizedResult(result);
         }
 
+        public object Get(GetRecommendedPrograms request)
+        {
+            var query = new RecommendedProgramQuery
+            {
+                UserId = request.UserId,
+                IsAiring = request.IsAiring,
+                Limit = request.Limit,
+                HasAired = request.HasAired
+            };
+
+            var result = _liveTvManager.GetRecommendedPrograms(query, CancellationToken.None).Result;
+
+            return ToOptimizedResult(result);
+        }
+
         public object Post(GetPrograms request)
         {
             return Get(request);

+ 3 - 2
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -1,6 +1,5 @@
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
-using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -734,7 +733,7 @@ namespace MediaBrowser.Api.Playback
                 return "-";
             }
 
-            var type = InputType.AudioFile;
+            var type = InputType.File;
 
             var inputPath = new[] { state.MediaPath };
 
@@ -1044,6 +1043,7 @@ namespace MediaBrowser.Api.Playback
                 }
 
                 itemId = recording.Id;
+                //state.RunTimeTicks = recording.RunTimeTicks;
                 state.SendInputOverStandardInput = recording.RecordingInfo.Status == RecordingStatus.InProgress;
             }
             else if (string.Equals(request.Type, "Channel", StringComparison.OrdinalIgnoreCase))
@@ -1092,6 +1092,7 @@ namespace MediaBrowser.Api.Playback
                         : video.PlayableStreamFileNames.ToList();
                 }
 
+                state.RunTimeTicks = item.RunTimeTicks;
                 itemId = item.Id;
             }
 

+ 1 - 1
MediaBrowser.Api/Playback/Hls/AudioHlsService.cs

@@ -1,9 +1,9 @@
 using MediaBrowser.Common.IO;
-using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaInfo;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.IO;

+ 18 - 11
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -1,11 +1,11 @@
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
-using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaInfo;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dto;
@@ -75,18 +75,23 @@ namespace MediaBrowser.Api.Playback.Hls
         /// <returns>System.Object.</returns>
         protected object ProcessRequest(StreamRequest request)
         {
-            var state = GetState(request, CancellationToken.None).Result;
-
-            return ProcessRequestAsync(state).Result;
+            return ProcessRequestAsync(request).Result;
         }
 
         /// <summary>
         /// Processes the request async.
         /// </summary>
-        /// <param name="state">The state.</param>
+        /// <param name="request">The request.</param>
         /// <returns>Task{System.Object}.</returns>
-        public async Task<object> ProcessRequestAsync(StreamState state)
+        /// <exception cref="ArgumentException">
+        /// A video bitrate is required
+        /// or
+        /// An audio bitrate is required
+        /// </exception>
+        private async Task<object> ProcessRequestAsync(StreamRequest request)
         {
+            var state = GetState(request, CancellationToken.None).Result;
+
             if (!state.VideoRequest.VideoBitRate.HasValue && (!state.VideoRequest.VideoCodec.HasValue || state.VideoRequest.VideoCodec.Value != VideoCodecs.Copy))
             {
                 throw new ArgumentException("A video bitrate is required");
@@ -155,7 +160,7 @@ namespace MediaBrowser.Api.Playback.Hls
         /// <param name="state">The state.</param>
         /// <param name="audioBitrate">The audio bitrate.</param>
         /// <param name="videoBitrate">The video bitrate.</param>
-        private void GetPlaylistBitrates(StreamState state, out int audioBitrate, out int videoBitrate)
+        protected void GetPlaylistBitrates(StreamState state, out int audioBitrate, out int videoBitrate)
         {
             var audioBitrateParam = GetAudioBitrateParam(state);
             var videoBitrateParam = GetVideoBitrateParam(state);
@@ -269,7 +274,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
             var threads = GetNumberOfThreads(false);
 
-            var args = string.Format("{0}{1} {2} {3} -i {4}{5} -map_metadata -1 -threads {6} {7} {8} -sc_threshold 0 {9} -hls_time 10 -start_number 0 -hls_list_size 1440 \"{10}\"",
+            var args = string.Format("{0}{1} {2} {3} -i {4}{5} -map_metadata -1 -threads {6} {7} {8} -sc_threshold 0 {9} -hls_time {10} -start_number 0 -hls_list_size 1440 \"{11}\"",
                 itsOffset,
                 probeSize,
                 GetUserAgentParam(state.MediaPath),
@@ -280,6 +285,7 @@ namespace MediaBrowser.Api.Playback.Hls
                 GetMapArgs(state),
                 GetVideoArguments(state, performSubtitleConversions),
                 GetAudioArguments(state),
+                state.SegmentLength.ToString(UsCulture),
                 outputPath
                 ).Trim();
 
@@ -291,10 +297,11 @@ namespace MediaBrowser.Api.Playback.Hls
 
                     var bitrate = hlsVideoRequest.BaselineStreamAudioBitRate ?? 64000;
 
-                    var lowBitrateParams = string.Format(" -threads {0} -vn -codec:a:0 libmp3lame -ac 2 -ab {2} -hls_time 10 -start_number 0 -hls_list_size 1440 \"{1}\"",
+                    var lowBitrateParams = string.Format(" -threads {0} -vn -codec:a:0 libmp3lame -ac 2 -ab {1} -hls_time {2} -start_number 0 -hls_list_size 1440 \"{3}\"",
                         threads,
-                        lowBitratePath,
-                        bitrate / 2);
+                        bitrate / 2,
+                        state.SegmentLength.ToString(UsCulture),
+                        lowBitratePath);
 
                     args += " " + lowBitrateParams;
                 }

+ 151 - 1
MediaBrowser.Api/Playback/Hls/VideoHlsService.cs

@@ -1,13 +1,18 @@
 using MediaBrowser.Common.IO;
-using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaInfo;
 using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.IO;
 using ServiceStack;
 using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
 
 namespace MediaBrowser.Api.Playback.Hls
 {
@@ -28,6 +33,29 @@ namespace MediaBrowser.Api.Playback.Hls
         public int TimeStampOffsetMs { get; set; }
     }
 
+    [Route("/Videos/{Id}/master.m3u8", "GET")]
+    [Api(Description = "Gets a video stream using HTTP live streaming.")]
+    public class GetMasterHlsVideoStream : VideoStreamRequest
+    {
+        [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; }
+    }
+
+    [Route("/Videos/{Id}/main.m3u8", "GET")]
+    [Api(Description = "Gets a video stream using HTTP live streaming.")]
+    public class GetMainHlsVideoStream : VideoStreamRequest
+    {
+    }
+
+    [Route("/Videos/{Id}/baseline.m3u8", "GET")]
+    [Api(Description = "Gets a video stream using HTTP live streaming.")]
+    public class GetBaselineHlsVideoStream : VideoStreamRequest
+    {
+    }
+
     /// <summary>
     /// Class VideoHlsService
     /// </summary>
@@ -38,6 +66,128 @@ namespace MediaBrowser.Api.Playback.Hls
         {
         }
 
+        public object Get(GetMasterHlsVideoStream request)
+        {
+            var result = GetAsync(request).Result;
+
+            return result;
+        }
+
+        public object Get(GetMainHlsVideoStream request)
+        {
+            var result = GetPlaylistAsync(request, "main").Result;
+
+            return result;
+        }
+
+        public object Get(GetBaselineHlsVideoStream request)
+        {
+            var result = GetPlaylistAsync(request, "baseline").Result;
+
+            return result;
+        }
+
+        private async Task<object> GetPlaylistAsync(VideoStreamRequest request, string name)
+        {
+            var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
+
+            var builder = new StringBuilder();
+
+            builder.AppendLine("#EXTM3U");
+            builder.AppendLine("#EXT-X-VERSION:3");
+            builder.AppendLine("#EXT-X-TARGETDURATION:" + state.SegmentLength.ToString(UsCulture));
+            builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
+
+            var queryStringIndex = Request.RawUrl.IndexOf('?');
+            var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
+            
+            var seconds = TimeSpan.FromTicks(state.RunTimeTicks ?? 0).TotalSeconds;
+
+            var index = 0;
+
+            while (seconds > 0)
+            {
+                var length = seconds >= state.SegmentLength ? state.SegmentLength : seconds;
+
+                builder.AppendLine("#EXTINF:" + length.ToString(UsCulture));
+
+                builder.AppendLine(string.Format("hls/{0}/{1}.ts{2}" ,
+
+                    name,
+                    index.ToString(UsCulture),
+                    queryString));
+
+                seconds -= state.SegmentLength;
+                index++;
+            }
+
+            builder.AppendLine("#EXT-X-ENDLIST");
+
+            var playlistText = builder.ToString();
+
+            return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
+        }
+
+        private async Task<object> GetAsync(GetMasterHlsVideoStream request)
+        {
+            var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
+
+            if (!state.VideoRequest.VideoBitRate.HasValue && (!state.VideoRequest.VideoCodec.HasValue || state.VideoRequest.VideoCodec.Value != VideoCodecs.Copy))
+            {
+                throw new ArgumentException("A video bitrate is required");
+            }
+            if (!state.Request.AudioBitRate.HasValue && (!state.Request.AudioCodec.HasValue || state.Request.AudioCodec.Value != AudioCodecs.Copy))
+            {
+                throw new ArgumentException("An audio bitrate is required");
+            }
+
+            int audioBitrate;
+            int videoBitrate;
+            GetPlaylistBitrates(state, out audioBitrate, out videoBitrate);
+
+            var appendBaselineStream = false;
+            var baselineStreamBitrate = 64000;
+
+            var hlsVideoRequest = state.VideoRequest as GetMasterHlsVideoStream;
+            if (hlsVideoRequest != null)
+            {
+                appendBaselineStream = hlsVideoRequest.AppendBaselineStream;
+                baselineStreamBitrate = hlsVideoRequest.BaselineStreamAudioBitRate ?? baselineStreamBitrate;
+            }
+
+            var playlistText = GetMasterPlaylistFileText(videoBitrate + audioBitrate, appendBaselineStream, baselineStreamBitrate);
+
+            return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
+        }
+
+        private string GetMasterPlaylistFileText(int bitrate, bool includeBaselineStream, int baselineStreamBitrate)
+        {
+            var builder = new StringBuilder();
+
+            builder.AppendLine("#EXTM3U");
+
+            // Pad a little to satisfy the apple hls validator
+            var paddedBitrate = Convert.ToInt32(bitrate * 1.05);
+
+            var queryStringIndex = Request.RawUrl.IndexOf('?');
+            var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
+
+            // Main stream
+            builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + paddedBitrate.ToString(UsCulture));
+            var playlistUrl = "main.m3u8" + queryString;
+            builder.AppendLine(playlistUrl);
+
+            // Low bitrate stream
+            if (includeBaselineStream)
+            {
+                builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + baselineStreamBitrate.ToString(UsCulture));
+                playlistUrl = "baseline.m3u8" + queryString;
+                builder.AppendLine(playlistUrl);
+            }
+
+            return builder.ToString();
+        }
+
         /// <summary>
         /// Gets the specified request.
         /// </summary>

+ 1 - 1
MediaBrowser.Api/Playback/Progressive/AudioService.cs

@@ -1,10 +1,10 @@
 using MediaBrowser.Common.IO;
-using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaInfo;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.IO;
 using ServiceStack;

+ 1 - 1
MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs

@@ -1,12 +1,12 @@
 using System;
 using MediaBrowser.Common.IO;
-using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaInfo;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.IO;

+ 1 - 1
MediaBrowser.Api/Playback/Progressive/VideoService.cs

@@ -1,10 +1,10 @@
 using MediaBrowser.Common.IO;
-using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaInfo;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.IO;
 using ServiceStack;

+ 6 - 2
MediaBrowser.Api/Playback/StreamState.cs

@@ -1,8 +1,8 @@
-using System.Threading;
-using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using System.Collections.Generic;
 using System.IO;
+using System.Threading;
 
 namespace MediaBrowser.Api.Playback
 {
@@ -54,5 +54,9 @@ namespace MediaBrowser.Api.Playback
         public CancellationTokenSource StandardInputCancellationTokenSource { get; set; }
 
         public string LiveTvStreamId { get; set; }
+
+        public int SegmentLength = 10;
+
+        public long? RunTimeTicks;
     }
 }

+ 0 - 2
MediaBrowser.Common/MediaBrowser.Common.csproj

@@ -65,8 +65,6 @@
     <Compile Include="IO\IFileSystem.cs" />
     <Compile Include="IO\ProgressStream.cs" />
     <Compile Include="IO\StreamDefaults.cs" />
-    <Compile Include="MediaInfo\MediaInfoResult.cs" />
-    <Compile Include="MediaInfo\IMediaEncoder.cs" />
     <Compile Include="Net\BasePeriodicWebSocketListener.cs" />
     <Compile Include="Configuration\IApplicationPaths.cs" />
     <Compile Include="Net\HttpRequestOptions.cs" />

+ 1 - 1
MediaBrowser.Controller/Entities/Audio/Audio.cs

@@ -109,7 +109,7 @@ namespace MediaBrowser.Controller.Entities.Audio
         /// <returns>System.String.</returns>
         public override string GetUserDataKey()
         {
-            var parent = Parent as MusicAlbum;
+            var parent = FindParent<MusicAlbum>();
 
             if (parent != null)
             {

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

@@ -1,5 +1,4 @@
-using System.IO;
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Querying;
 using System.Collections.Generic;
@@ -241,5 +240,14 @@ namespace MediaBrowser.Controller.LiveTv
         /// </summary>
         /// <returns>GuideInfo.</returns>
         GuideInfo GetGuideInfo();
+
+        /// <summary>
+        /// Gets the recommended programs.
+        /// </summary>
+        /// <param name="query">The query.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{QueryResult{ProgramInfoDto}}.</returns>
+        Task<QueryResult<ProgramInfoDto>> GetRecommendedPrograms(RecommendedProgramQuery query,
+            CancellationToken cancellationToken);
     }
 }

+ 18 - 7
MediaBrowser.Controller/LiveTv/ILiveTvService.cs

@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
 
@@ -37,7 +38,7 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         Task CancelSeriesTimerAsync(string timerId, CancellationToken cancellationToken);
-        
+
         /// <summary>
         /// Deletes the recording asynchronous.
         /// </summary>
@@ -77,7 +78,7 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken);
-        
+
         /// <summary>
         /// Gets the channel image asynchronous. This only needs to be implemented if an image path or url cannot be supplied to ChannelInfo
         /// </summary>
@@ -102,7 +103,7 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{ImageResponseInfo}.</returns>
         Task<StreamResponseInfo> GetProgramImageAsync(string programId, string channelId, CancellationToken cancellationToken);
-        
+
         /// <summary>
         /// Gets the recordings asynchronous.
         /// </summary>
@@ -123,21 +124,23 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{TimerInfo}.</returns>
         Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken);
-        
+
         /// <summary>
         /// Gets the series timers asynchronous.
         /// </summary>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{IEnumerable{SeriesTimerInfo}}.</returns>
         Task<IEnumerable<SeriesTimerInfo>> GetSeriesTimersAsync(CancellationToken cancellationToken);
-        
+
         /// <summary>
         /// Gets the programs asynchronous.
         /// </summary>
         /// <param name="channelId">The channel identifier.</param>
+        /// <param name="startDateUtc">The start date UTC.</param>
+        /// <param name="endDateUtc">The end date UTC.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{IEnumerable{ProgramInfo}}.</returns>
-        Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, CancellationToken cancellationToken);
+        Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken);
 
         /// <summary>
         /// Gets the recording stream.
@@ -162,5 +165,13 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         Task CloseLiveStream(string id, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Records the live stream.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task RecordLiveStream(string id, CancellationToken cancellationToken);
     }
 }

+ 21 - 0
MediaBrowser.Controller/LiveTv/LiveTvProgram.cs

@@ -1,5 +1,6 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.LiveTv;
+using System;
 
 namespace MediaBrowser.Controller.LiveTv
 {
@@ -28,6 +29,26 @@ namespace MediaBrowser.Controller.LiveTv
             }
         }
 
+        public bool IsAiring
+        {
+            get
+            {
+                var now = DateTime.UtcNow;
+
+                return now >= ProgramInfo.StartDate && now < ProgramInfo.EndDate;
+            }
+        }
+
+        public bool HasAired
+        {
+            get
+            {
+                var now = DateTime.UtcNow;
+
+                return now >= ProgramInfo.EndDate;
+            }
+        }
+
         public override string GetClientTypeName()
         {
             return "Program";

+ 2 - 0
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -125,6 +125,8 @@
     <Compile Include="LiveTv\SeriesTimerInfo.cs" />
     <Compile Include="LiveTv\TimerInfo.cs" />
     <Compile Include="Localization\ILocalizationManager.cs" />
+    <Compile Include="MediaInfo\IMediaEncoder.cs" />
+    <Compile Include="MediaInfo\InternalMediaInfoResult.cs" />
     <Compile Include="Net\IHasResultFactory.cs" />
     <Compile Include="Net\IHttpResultFactory.cs" />
     <Compile Include="Net\IHttpServer.cs" />

+ 1 - 2
MediaBrowser.Controller/MediaInfo/FFMpegManager.cs

@@ -1,6 +1,5 @@
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
-using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
@@ -178,7 +177,7 @@ namespace MediaBrowser.Controller.MediaInfo
 
                             Directory.CreateDirectory(parentPath);
 
-                            await _encoder.ExtractImage(inputPath, type, video.Video3DFormat, time, path, cancellationToken).ConfigureAwait(false);
+                            await _encoder.ExtractImage(inputPath, type, false, video.Video3DFormat, time, path, cancellationToken).ConfigureAwait(false);
                             chapter.ImagePath = path;
                             changesMade = true;
                         }

+ 6 - 9
MediaBrowser.Common/MediaInfo/IMediaEncoder.cs → MediaBrowser.Controller/MediaInfo/IMediaEncoder.cs

@@ -3,7 +3,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Model.Entities;
 
-namespace MediaBrowser.Common.MediaInfo
+namespace MediaBrowser.Controller.MediaInfo
 {
     /// <summary>
     /// Interface IMediaEncoder
@@ -27,12 +27,13 @@ namespace MediaBrowser.Common.MediaInfo
         /// </summary>
         /// <param name="inputFiles">The input files.</param>
         /// <param name="type">The type.</param>
+        /// <param name="isAudio">if set to <c>true</c> [is audio].</param>
         /// <param name="threedFormat">The threed format.</param>
         /// <param name="offset">The offset.</param>
         /// <param name="outputPath">The output path.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        Task ExtractImage(string[] inputFiles, InputType type, Video3DFormat? threedFormat, TimeSpan? offset, string outputPath, CancellationToken cancellationToken);
+        Task ExtractImage(string[] inputFiles, InputType type, bool isAudio, Video3DFormat? threedFormat, TimeSpan? offset, string outputPath, CancellationToken cancellationToken);
 
         /// <summary>
         /// Extracts the text subtitle.
@@ -62,7 +63,7 @@ namespace MediaBrowser.Common.MediaInfo
         /// <param name="type">The type.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        Task<MediaInfoResult> GetMediaInfo(string[] inputFiles, InputType type, CancellationToken cancellationToken);
+        Task<InternalMediaInfoResult> GetMediaInfo(string[] inputFiles, InputType type, CancellationToken cancellationToken);
 
         /// <summary>
         /// Gets the probe size argument.
@@ -86,13 +87,9 @@ namespace MediaBrowser.Common.MediaInfo
     public enum InputType
     {
         /// <summary>
-        /// The audio file
+        /// The file
         /// </summary>
-        AudioFile,
-        /// <summary>
-        /// The video file
-        /// </summary>
-        VideoFile,
+        File,
         /// <summary>
         /// The bluray
         /// </summary>

+ 4 - 4
MediaBrowser.Common/MediaInfo/MediaInfoResult.cs → MediaBrowser.Controller/MediaInfo/InternalMediaInfoResult.cs

@@ -1,12 +1,12 @@
-using MediaBrowser.Model.Entities;
-using System.Collections.Generic;
+using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
 
-namespace MediaBrowser.Common.MediaInfo
+namespace MediaBrowser.Controller.MediaInfo
 {
     /// <summary>
     /// Class MediaInfoResult
     /// </summary>
-    public class MediaInfoResult
+    public class InternalMediaInfoResult
     {
         /// <summary>
         /// Gets or sets the streams.

+ 15 - 6
MediaBrowser.Controller/MediaInfo/MediaEncoderHelpers.cs

@@ -3,7 +3,6 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
-using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 
@@ -29,7 +28,7 @@ namespace MediaBrowser.Controller.MediaInfo
         {
             var inputPath = isoMount == null ? new[] { videoPath } : new[] { isoMount.MountedPath };
 
-            type = InputType.VideoFile;
+            type = InputType.File;
 
             switch (videoType)
             {
@@ -87,7 +86,7 @@ namespace MediaBrowser.Controller.MediaInfo
         /// <returns>InputType.</returns>
         public static InputType GetInputType(VideoType? videoType, IsoType? isoType)
         {
-            var type = InputType.AudioFile;
+            var type = InputType.File;
 
             if (videoType.HasValue)
             {
@@ -119,12 +118,22 @@ namespace MediaBrowser.Controller.MediaInfo
             return type;
         }
 
-        public static IEnumerable<MediaStream> GetMediaStreams(MediaInfoResult data)
+        public static Model.Entities.MediaInfo GetMediaInfo(InternalMediaInfoResult data)
         {
             var internalStreams = data.streams ?? new MediaStreamInfo[] { };
 
-            return internalStreams.Select(s => GetMediaStream(s, data.format))
-                .Where(i => i != null);
+            var info = new Model.Entities.MediaInfo();
+
+            info.MediaStreams = internalStreams.Select(s => GetMediaStream(s, data.format))
+                .Where(i => i != null)
+                .ToList();
+
+            if (data.format != null)
+            {
+                info.Format = data.format.format_name;
+            }
+
+            return info;
         }
 
         private static readonly CultureInfo UsCulture = new CultureInfo("en-US");

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

@@ -223,7 +223,10 @@ namespace MediaBrowser.Model.Configuration
         public string TranscodingTempPath { get; set; }
 
         public bool EnableAutomaticRestart { get; set; }
-        
+
+
+        public LiveTvOptions LiveTvOptions { get; set; }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
         /// </summary>
@@ -287,6 +290,8 @@ namespace MediaBrowser.Model.Configuration
             {
                  MaxBackdrops = 1
             };
+
+            LiveTvOptions = new LiveTvOptions();
         }
     }
 
@@ -303,4 +308,9 @@ namespace MediaBrowser.Model.Configuration
         HighQuality,
         MaxQuality
     }
+
+    public class LiveTvOptions
+    {
+        public int? GuideDays { get; set; }
+    }
 }

+ 3 - 1
MediaBrowser.Model/Dto/BaseItemDto.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Model.Entities;
+using System.Diagnostics;
+using MediaBrowser.Model.Entities;
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
@@ -10,6 +11,7 @@ namespace MediaBrowser.Model.Dto
     /// This is strictly used as a data transfer object from the api layer.
     /// This holds information about a BaseItem in a format that is convenient for the client.
     /// </summary>
+    [DebuggerDisplay("Name = {Name}, ID = {Id}, Type = {Type}")]
     public class BaseItemDto : IHasProviderIds, INotifyPropertyChanged, IItemDto
     {
         /// <summary>

+ 2 - 0
MediaBrowser.Model/Dto/BaseItemPerson.cs

@@ -1,5 +1,6 @@
 using System;
 using System.ComponentModel;
+using System.Diagnostics;
 using System.Runtime.Serialization;
 
 namespace MediaBrowser.Model.Dto
@@ -7,6 +8,7 @@ namespace MediaBrowser.Model.Dto
     /// <summary>
     /// This is used by the api to get information about a Person within a BaseItem
     /// </summary>
+    [DebuggerDisplay("Name = {Name}, Role = {Role}, Type = {Type}")]
     public class BaseItemPerson : INotifyPropertyChanged
     {
         /// <summary>

+ 2 - 0
MediaBrowser.Model/Dto/ChapterInfoDto.cs

@@ -1,5 +1,6 @@
 using System;
 using System.ComponentModel;
+using System.Diagnostics;
 using System.Runtime.Serialization;
 
 namespace MediaBrowser.Model.Dto
@@ -7,6 +8,7 @@ namespace MediaBrowser.Model.Dto
     /// <summary>
     /// Class ChapterInfo
     /// </summary>
+    [DebuggerDisplay("Name = {Name}")]
     public class ChapterInfoDto : INotifyPropertyChanged
     {
         /// <summary>

+ 2 - 0
MediaBrowser.Model/Dto/StudioDto.cs

@@ -1,5 +1,6 @@
 using System;
 using System.ComponentModel;
+using System.Diagnostics;
 using System.Runtime.Serialization;
 
 namespace MediaBrowser.Model.Dto
@@ -7,6 +8,7 @@ namespace MediaBrowser.Model.Dto
     /// <summary>
     /// Class StudioDto
     /// </summary>
+    [DebuggerDisplay("Name = {Name}")]
     public class StudioDto
     {
         /// <summary>

+ 2 - 0
MediaBrowser.Model/Dto/UserDto.cs

@@ -1,4 +1,5 @@
 using System.ComponentModel;
+using System.Diagnostics;
 using MediaBrowser.Model.Configuration;
 using System;
 using System.Runtime.Serialization;
@@ -8,6 +9,7 @@ namespace MediaBrowser.Model.Dto
     /// <summary>
     /// Class UserDto
     /// </summary>
+    [DebuggerDisplay("Name = {Name}, ID = {Id}, HasPassword = {HasPassword}")]
     public class UserDto : INotifyPropertyChanged, IItemDto
     {
         /// <summary>

+ 2 - 0
MediaBrowser.Model/Entities/BaseItemInfo.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Diagnostics;
 using System.Runtime.Serialization;
 
 namespace MediaBrowser.Model.Entities
@@ -6,6 +7,7 @@ namespace MediaBrowser.Model.Entities
     /// <summary>
     /// This is a stub class containing only basic information about an item
     /// </summary>
+    [DebuggerDisplay("Name = {Name}, ID = {Id}, Type = {Type}")]
     public class BaseItemInfo
     {
         /// <summary>

+ 24 - 1
MediaBrowser.Model/Entities/MediaStream.cs

@@ -1,9 +1,12 @@
-
+using System.Collections.Generic;
+using System.Diagnostics;
+
 namespace MediaBrowser.Model.Entities
 {
     /// <summary>
     /// Class MediaStream
     /// </summary>
+    [DebuggerDisplay("StreamType = {Type}")]
     public class MediaStream
     {
         /// <summary>
@@ -145,4 +148,24 @@ namespace MediaBrowser.Model.Entities
         /// </summary>
         Subtitle
     }
+
+    public class MediaInfo
+    {
+        /// <summary>
+        /// Gets or sets the media streams.
+        /// </summary>
+        /// <value>The media streams.</value>
+        public List<MediaStream> MediaStreams { get; set; }
+
+        /// <summary>
+        /// Gets or sets the format.
+        /// </summary>
+        /// <value>The format.</value>
+        public string Format { get; set; }
+
+        public MediaInfo()
+        {
+            MediaStreams = new List<MediaStream>();
+        }
+    }
 }

+ 27 - 0
MediaBrowser.Model/LiveTv/ProgramQuery.cs

@@ -32,4 +32,31 @@ namespace MediaBrowser.Model.LiveTv
             ChannelIdList = new string[] { };
         }
     }
+
+    public class RecommendedProgramQuery
+    {
+        /// <summary>
+        /// Gets or sets the user identifier.
+        /// </summary>
+        /// <value>The user identifier.</value>
+        public string UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance is airing.
+        /// </summary>
+        /// <value><c>true</c> if this instance is airing; otherwise, <c>false</c>.</value>
+        public bool? IsAiring { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance has aired.
+        /// </summary>
+        /// <value><c>null</c> if [has aired] contains no value, <c>true</c> if [has aired]; otherwise, <c>false</c>.</value>
+        public bool? HasAired { get; set; }
+
+        /// <summary>
+        /// The maximum number of items to return
+        /// </summary>
+        /// <value>The limit.</value>
+        public int? Limit { get; set; }
+    }
 }

+ 3 - 1
MediaBrowser.Model/Session/SessionInfoDto.cs

@@ -1,10 +1,12 @@
-using MediaBrowser.Model.Entities;
+using System.Diagnostics;
+using MediaBrowser.Model.Entities;
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
 
 namespace MediaBrowser.Model.Session
 {
+    [DebuggerDisplay("Client = {Client}, Username = {UserName}")]
     public class SessionInfoDto : INotifyPropertyChanged
     {
         /// <summary>

+ 2 - 2
MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs

@@ -1,9 +1,9 @@
 using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaInfo;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
@@ -163,7 +163,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
                         Directory.CreateDirectory(parentPath);
 
-                        await _mediaEncoder.ExtractImage(new[] { item.Path }, InputType.AudioFile, null, null, path, cancellationToken).ConfigureAwait(false);
+                        await _mediaEncoder.ExtractImage(new[] { item.Path }, InputType.File, true, null, null, path, cancellationToken).ConfigureAwait(false);
                     }
                     finally
                     {

+ 4 - 5
MediaBrowser.Providers/MediaInfo/BaseFFProbeProvider.cs

@@ -1,5 +1,4 @@
-using MediaBrowser.Common.MediaInfo;
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.MediaInfo;
 using MediaBrowser.Controller.Providers;
@@ -104,11 +103,11 @@ namespace MediaBrowser.Providers.MediaInfo
         /// <exception cref="System.ArgumentNullException">inputPath
         /// or
         /// cache</exception>
-        protected async Task<MediaInfoResult> GetMediaInfo(BaseItem item, IIsoMount isoMount, CancellationToken cancellationToken)
+        protected async Task<InternalMediaInfoResult> GetMediaInfo(BaseItem item, IIsoMount isoMount, CancellationToken cancellationToken)
         {
             cancellationToken.ThrowIfCancellationRequested();
 
-            var type = InputType.AudioFile;
+            var type = InputType.File;
             var inputPath = isoMount == null ? new[] { item.Path } : new[] { isoMount.MountedPath };
 
             var video = item as Video;
@@ -146,7 +145,7 @@ namespace MediaBrowser.Providers.MediaInfo
         /// Normalizes the FF probe result.
         /// </summary>
         /// <param name="result">The result.</param>
-        protected void NormalizeFFProbeResult(MediaInfoResult result)
+        protected void NormalizeFFProbeResult(InternalMediaInfoResult result)
         {
             if (result.format != null && result.format.tags != null)
             {

+ 2 - 3
MediaBrowser.Providers/MediaInfo/FFProbeAudioInfoProvider.cs

@@ -1,5 +1,4 @@
 using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -58,9 +57,9 @@ namespace MediaBrowser.Providers.MediaInfo
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="data">The data.</param>
         /// <returns>Task.</returns>
-        protected Task Fetch(Audio audio, CancellationToken cancellationToken, MediaInfoResult data)
+        protected Task Fetch(Audio audio, CancellationToken cancellationToken, InternalMediaInfoResult data)
         {
-            var mediaStreams = MediaEncoderHelpers.GetMediaStreams(data).ToList();
+            var mediaStreams = MediaEncoderHelpers.GetMediaInfo(data).MediaStreams;
 
             audio.HasEmbeddedImage = mediaStreams.Any(i => i.Type == MediaStreamType.Video);
 

+ 3 - 4
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfoProvider.cs

@@ -1,5 +1,4 @@
 using DvdLib.Ifo;
-using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Localization;
@@ -310,7 +309,7 @@ namespace MediaBrowser.Providers.MediaInfo
         /// <param name="data">The data.</param>
         /// <param name="isoMount">The iso mount.</param>
         /// <returns>Task.</returns>
-        protected async Task Fetch(Video video, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken, MediaInfoResult data, IIsoMount isoMount)
+        protected async Task Fetch(Video video, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken, InternalMediaInfoResult data, IIsoMount isoMount)
         {
             if (data.format != null)
             {
@@ -323,7 +322,7 @@ namespace MediaBrowser.Providers.MediaInfo
                 }
             }
 
-            var mediaStreams = MediaEncoderHelpers.GetMediaStreams(data).ToList();
+            var mediaStreams = MediaEncoderHelpers.GetMediaInfo(data).MediaStreams;
 
             var chapters = data.Chapters ?? new List<ChapterInfo>();
 
@@ -370,7 +369,7 @@ namespace MediaBrowser.Providers.MediaInfo
         /// <param name="video">The video.</param>
         /// <param name="force">if set to <c>true</c> [force].</param>
         /// <param name="data">The data.</param>
-        private void FetchWtvInfo(Video video, bool force, MediaInfoResult data)
+        private void FetchWtvInfo(Video video, bool force, InternalMediaInfoResult data)
         {
             if (data.format == null || data.format.tags == null)
             {

+ 1 - 2
MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs

@@ -1,5 +1,4 @@
 using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -255,7 +254,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
                 var inputPath = MediaEncoderHelpers.GetInputArgument(video.Path, video.LocationType == LocationType.Remote, video.VideoType, video.IsoType, isoMount, video.PlayableStreamFileNames, out type);
 
-                await _mediaEncoder.ExtractImage(inputPath, type, video.Video3DFormat, imageOffset, path, cancellationToken).ConfigureAwait(false);
+                await _mediaEncoder.ExtractImage(inputPath, type, false, video.Video3DFormat, imageOffset, path, cancellationToken).ConfigureAwait(false);
 
                 video.PrimaryImagePath = path;
             }

+ 4 - 12
MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs

@@ -360,19 +360,11 @@ namespace MediaBrowser.Server.Implementations.HttpServer
 
             var compress = ShouldCompressResponse(requestContext, contentType);
 
-            var hasOptions = GetStaticResult(requestContext, responseHeaders, contentType, factoryFn, compress, isHeadRequest);
+            var hasOptions = GetStaticResult(requestContext, responseHeaders, contentType, factoryFn, compress, isHeadRequest).Result;
 
-            return GetStaticResultTask(hasOptions, responseHeaders);
-        }
-
-        private async Task<object> GetStaticResultTask(Task<IHasOptions> optionsTask,
-            IEnumerable<KeyValuePair<string, string>> responseHeaders)
-        {
-            var options = await optionsTask.ConfigureAwait(false);
-
-            AddResponseHeaders(options, responseHeaders);
+            AddResponseHeaders(hasOptions, responseHeaders);
 
-            return options;
+            return hasOptions;
         }
 
         /// <summary>
@@ -670,4 +662,4 @@ namespace MediaBrowser.Server.Implementations.HttpServer
             throw error;
         }
     }
-}
+}

+ 8 - 0
MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs

@@ -2,6 +2,7 @@
 using ServiceStack.Web;
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.IO;
 using System.Threading.Tasks;
 
@@ -13,6 +14,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer
     public class StreamWriter : IStreamWriter, IHasOptions
     {
         private ILogger Logger { get; set; }
+
+        private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
         
         /// <summary>
         /// Gets or sets the source stream.
@@ -50,6 +53,11 @@ namespace MediaBrowser.Server.Implementations.HttpServer
             Logger = logger;
 
             Options["Content-Type"] = contentType;
+
+            if (source.CanSeek)
+            {
+                Options["Content-Length"] = source.Length.ToString(UsCulture);
+            }
         }
 
         /// <summary>

+ 1 - 1
MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs

@@ -368,7 +368,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return null;
         }
 
-        private const string InternalVersionNumber = "2";
+        private const string InternalVersionNumber = "3";
 
         public Guid GetInternalChannelId(string serviceName, string externalId)
         {

+ 217 - 9
MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -1,17 +1,20 @@
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaInfo;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Querying;
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
@@ -23,30 +26,37 @@ namespace MediaBrowser.Server.Implementations.LiveTv
     /// <summary>
     /// Class LiveTvManager
     /// </summary>
-    public class LiveTvManager : ILiveTvManager
+    public class LiveTvManager : ILiveTvManager, IDisposable
     {
-        private readonly IServerApplicationPaths _appPaths;
+        private readonly IServerConfigurationManager _config;
         private readonly IFileSystem _fileSystem;
         private readonly ILogger _logger;
         private readonly IItemRepository _itemRepo;
         private readonly IUserManager _userManager;
+        private readonly IUserDataManager _userDataManager;
         private readonly ILibraryManager _libraryManager;
+        private readonly IMediaEncoder _mediaEncoder;
 
         private readonly LiveTvDtoService _tvDtoService;
 
         private readonly List<ILiveTvService> _services = new List<ILiveTvService>();
 
+        private readonly ConcurrentDictionary<string, LiveStreamInfo> _openStreams =
+            new ConcurrentDictionary<string, LiveStreamInfo>();
+
         private List<Guid> _channelIdList = new List<Guid>();
         private Dictionary<Guid, LiveTvProgram> _programs = new Dictionary<Guid, LiveTvProgram>();
 
-        public LiveTvManager(IServerApplicationPaths appPaths, IFileSystem fileSystem, ILogger logger, IItemRepository itemRepo, IImageProcessor imageProcessor, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager, ILibraryManager libraryManager)
+        public LiveTvManager(IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger, IItemRepository itemRepo, IImageProcessor imageProcessor, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager, ILibraryManager libraryManager, IMediaEncoder mediaEncoder)
         {
-            _appPaths = appPaths;
+            _config = config;
             _fileSystem = fileSystem;
             _logger = logger;
             _itemRepo = itemRepo;
             _userManager = userManager;
             _libraryManager = libraryManager;
+            _mediaEncoder = mediaEncoder;
+            _userDataManager = userDataManager;
 
             _tvDtoService = new LiveTvDtoService(dtoService, userDataManager, imageProcessor, logger, _itemRepo);
         }
@@ -180,7 +190,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
             var recording = recordings.First(i => _tvDtoService.GetInternalRecordingId(service.Name, i.Id) == new Guid(id));
 
-            return await service.GetRecordingStream(recording.Id, cancellationToken).ConfigureAwait(false);
+            var result = await service.GetRecordingStream(recording.Id, cancellationToken).ConfigureAwait(false);
+
+            if (!string.IsNullOrEmpty(result.Id))
+            {
+                _openStreams.AddOrUpdate(result.Id, result, (key, info) => result);
+            }
+
+            return result;
         }
 
         public async Task<LiveStreamInfo> GetChannelStream(string id, CancellationToken cancellationToken)
@@ -189,12 +206,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
             var channel = GetInternalChannel(id);
 
-            return await service.GetChannelStream(channel.ChannelInfo.Id, cancellationToken).ConfigureAwait(false);
+            var result = await service.GetChannelStream(channel.ChannelInfo.Id, cancellationToken).ConfigureAwait(false);
+
+            if (!string.IsNullOrEmpty(result.Id))
+            {
+                _openStreams.AddOrUpdate(result.Id, result, (key, info) => result);
+            }
+
+            return result;
         }
 
         private async Task<LiveTvChannel> GetChannel(ChannelInfo channelInfo, string serviceName, CancellationToken cancellationToken)
         {
-            var path = Path.Combine(_appPaths.ItemsByNamePath, "channels", _fileSystem.GetValidFilename(serviceName), _fileSystem.GetValidFilename(channelInfo.Name));
+            var path = Path.Combine(_config.ApplicationPaths.ItemsByNamePath, "channels", _fileSystem.GetValidFilename(channelInfo.Name));
 
             var fileInfo = new DirectoryInfo(path);
 
@@ -407,7 +431,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             }
 
             var returnArray = programs
-                .OrderBy(i => i.ProgramInfo.StartDate)
                 .Select(i =>
                 {
                     var channel = GetChannel(i);
@@ -429,6 +452,138 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             return result;
         }
 
+        public async Task<QueryResult<ProgramInfoDto>> GetRecommendedPrograms(RecommendedProgramQuery query, CancellationToken cancellationToken)
+        {
+            IEnumerable<LiveTvProgram> programs = _programs.Values;
+
+            var user = _userManager.GetUserById(new Guid(query.UserId));
+
+            // Avoid implicitly captured closure
+            var currentUser = user;
+            programs = programs.Where(i => i.IsParentalAllowed(currentUser));
+
+            if (query.IsAiring.HasValue)
+            {
+                var val = query.IsAiring.Value;
+                programs = programs.Where(i => i.IsAiring == val);
+            }
+
+            if (query.HasAired.HasValue)
+            {
+                var val = query.HasAired.Value;
+                programs = programs.Where(i => i.HasAired == val);
+            }
+
+            var serviceName = ActiveService.Name;
+
+            var programList = programs.ToList();
+
+            var genres = programList.SelectMany(i => i.Genres)
+                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .Select(i => _libraryManager.GetGenre(i))
+                .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
+
+            programs = programList.OrderByDescending(i => GetRecommendationScore(i.ProgramInfo, user.Id, serviceName, genres))
+                .ThenBy(i => i.ProgramInfo.StartDate);
+
+            if (query.Limit.HasValue)
+            {
+                programs = programs.Take(query.Limit.Value)
+                    .OrderBy(i => i.ProgramInfo.StartDate);
+            }
+
+            var returnArray = programs
+                .Select(i =>
+                {
+                    var channel = GetChannel(i);
+
+                    var channelName = channel == null ? null : channel.ChannelInfo.Name;
+
+                    return _tvDtoService.GetProgramInfoDto(i, channelName, user);
+                })
+                .ToArray();
+
+            await AddRecordingInfo(returnArray, cancellationToken).ConfigureAwait(false);
+
+            var result = new QueryResult<ProgramInfoDto>
+            {
+                Items = returnArray,
+                TotalRecordCount = returnArray.Length
+            };
+
+            return result;
+        }
+
+        private int GetRecommendationScore(ProgramInfo program, Guid userId, string serviceName, Dictionary<string, Genre> genres)
+        {
+            var score = 0;
+
+            if (program.IsLive)
+            {
+                score++;
+            }
+
+            if (program.IsSeries && !program.IsRepeat)
+            {
+                score++;
+            }
+
+            var internalChannelId = _tvDtoService.GetInternalChannelId(serviceName, program.ChannelId);
+            var channel = GetInternalChannel(internalChannelId);
+
+            var channelUserdata = _userDataManager.GetUserData(userId, channel.GetUserDataKey());
+
+            if ((channelUserdata.Likes ?? false))
+            {
+                score += 2;
+            }
+            else if (!(channelUserdata.Likes ?? true))
+            {
+                score -= 2;
+            }
+
+            if (channelUserdata.IsFavorite)
+            {
+                score += 3;
+            }
+
+            score += GetGenreScore(program.Genres, userId, genres);
+
+            return score;
+        }
+
+        private int GetGenreScore(IEnumerable<string> programGenres, Guid userId, Dictionary<string, Genre> genres)
+        {
+            return programGenres.Select(i =>
+            {
+                var score = 0;
+
+                Genre genre;
+
+                if (genres.TryGetValue(i, out genre))
+                {
+                    var genreUserdata = _userDataManager.GetUserData(userId, genre.GetUserDataKey());
+
+                    if ((genreUserdata.Likes ?? false))
+                    {
+                        score++;
+                    }
+                    else if (!(genreUserdata.Likes ?? true))
+                    {
+                        score--;
+                    }
+
+                    if (genreUserdata.IsFavorite)
+                    {
+                        score += 2;
+                    }
+                }
+
+                return score;
+
+            }).Sum();
+        }
+
         private async Task AddRecordingInfo(IEnumerable<ProgramInfoDto> programs, CancellationToken cancellationToken)
         {
             var timers = await ActiveService.GetTimersAsync(cancellationToken).ConfigureAwait(false);
@@ -505,6 +660,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             numComplete = 0;
             var programs = new List<LiveTvProgram>();
 
+            var guideDays = GetGuideDays(list.Count);
+
             foreach (var item in list)
             {
                 // Avoid implicitly captured closure
@@ -512,7 +669,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
                 try
                 {
-                    var channelPrograms = await service.GetProgramsAsync(currentChannel.ChannelInfo.Id, cancellationToken).ConfigureAwait(false);
+                    var start = DateTime.UtcNow.AddHours(-1);
+                    var end = start.AddDays(guideDays);
+
+                    var channelPrograms = await service.GetProgramsAsync(currentChannel.ChannelInfo.Id, start, end, cancellationToken).ConfigureAwait(false);
 
                     var programTasks = channelPrograms.Select(program => GetProgram(program, currentChannel.ChannelInfo.ChannelType, service.Name, cancellationToken));
                     var programEntities = await Task.WhenAll(programTasks).ConfigureAwait(false);
@@ -538,6 +698,23 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             _programs = programs.ToDictionary(i => i.Id);
         }
 
+        private double GetGuideDays(int channelCount)
+        {
+            if (_config.Configuration.LiveTvOptions.GuideDays.HasValue)
+            {
+                return _config.Configuration.LiveTvOptions.GuideDays.Value;
+            }
+
+            var programsPerDay = channelCount * 48;
+
+            const int maxPrograms = 32000;
+
+            var days = Math.Round(((double)maxPrograms) / programsPerDay);
+
+            // No less than 2, no more than 14
+            return Math.Max(2, Math.Min(days, 14));
+        }
+
         private async Task<IEnumerable<Tuple<string, ChannelInfo>>> GetChannels(ILiveTvService service, CancellationToken cancellationToken)
         {
             var channels = await service.GetChannelsAsync(cancellationToken).ConfigureAwait(false);
@@ -1047,5 +1224,36 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 EndDate = endDate
             };
         }
+
+        /// <summary>
+        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+        /// </summary>
+        public void Dispose()
+        {
+            Dispose(true);
+        }
+
+        private readonly object _disposeLock = new object();
+        /// <summary>
+        /// Releases unmanaged and - optionally - managed resources.
+        /// </summary>
+        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+        protected virtual void Dispose(bool dispose)
+        {
+            if (dispose)
+            {
+                lock (_disposeLock)
+                {
+                    foreach (var stream in _openStreams.Values.ToList())
+                    {
+                        var task = CloseLiveStream(stream.Id, CancellationToken.None);
+
+                        Task.WaitAll(task);
+                    }
+
+                    _openStreams.Clear();
+                }
+            }
+        }
     }
 }

+ 12 - 12
MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs

@@ -1,6 +1,6 @@
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.IO;
-using MediaBrowser.Common.MediaInfo;
+using MediaBrowser.Controller.MediaInfo;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Serialization;
@@ -104,10 +104,10 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
         /// <param name="type">The type.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        public Task<MediaInfoResult> GetMediaInfo(string[] inputFiles, InputType type,
+        public Task<InternalMediaInfoResult> GetMediaInfo(string[] inputFiles, InputType type,
                                                   CancellationToken cancellationToken)
         {
-            return GetMediaInfoInternal(GetInputArgument(inputFiles, type), type != InputType.AudioFile,
+            return GetMediaInfoInternal(GetInputArgument(inputFiles, type), type != InputType.File,
                                         GetProbeSizeArgument(type), cancellationToken);
         }
 
@@ -125,8 +125,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
             switch (type)
             {
                 case InputType.Dvd:
-                case InputType.VideoFile:
-                case InputType.AudioFile:
+                case InputType.File:
                     inputPath = GetConcatInputArgument(inputFiles);
                     break;
                 case InputType.Bluray:
@@ -173,7 +172,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{MediaInfoResult}.</returns>
         /// <exception cref="System.ApplicationException"></exception>
-        private async Task<MediaInfoResult> GetMediaInfoInternal(string inputPath, bool extractChapters,
+        private async Task<InternalMediaInfoResult> GetMediaInfoInternal(string inputPath, bool extractChapters,
                                                                  string probeSizeArgument,
                                                                  CancellationToken cancellationToken)
         {
@@ -206,7 +205,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
 
             await _ffProbeResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
 
-            MediaInfoResult result;
+            InternalMediaInfoResult result;
             string standardError = null;
 
             try
@@ -236,7 +235,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
                     process.BeginErrorReadLine();
                 }
 
-                result = _jsonSerializer.DeserializeFromStream<MediaInfoResult>(process.StandardOutput.BaseStream);
+                result = _jsonSerializer.DeserializeFromStream<InternalMediaInfoResult>(process.StandardOutput.BaseStream);
 
                 if (extractChapters)
                 {
@@ -307,7 +306,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
         /// </summary>
         /// <param name="result">The result.</param>
         /// <param name="standardError">The standard error.</param>
-        private void AddChapters(MediaInfoResult result, string standardError)
+        private void AddChapters(InternalMediaInfoResult result, string standardError)
         {
             var lines = standardError.Split('\n').Select(l => l.TrimStart());
 
@@ -797,19 +796,20 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
         /// </summary>
         /// <param name="inputFiles">The input files.</param>
         /// <param name="type">The type.</param>
+        /// <param name="isAudio">if set to <c>true</c> [is audio].</param>
         /// <param name="threedFormat">The threed format.</param>
         /// <param name="offset">The offset.</param>
         /// <param name="outputPath">The output path.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         /// <exception cref="System.ArgumentException">Must use inputPath list overload</exception>
-        public async Task ExtractImage(string[] inputFiles, InputType type, Video3DFormat? threedFormat, TimeSpan? offset, string outputPath, CancellationToken cancellationToken)
+        public async Task ExtractImage(string[] inputFiles, InputType type, bool isAudio, Video3DFormat? threedFormat, TimeSpan? offset, string outputPath, CancellationToken cancellationToken)
         {
-            var resourcePool = type == InputType.AudioFile ? _audioImageResourcePool : _videoImageResourcePool;
+            var resourcePool = isAudio ? _audioImageResourcePool : _videoImageResourcePool;
 
             var inputArgument = GetInputArgument(inputFiles, type);
 
-            if (type != InputType.AudioFile)
+            if (!isAudio)
             {
                 try
                 {

+ 3 - 4
MediaBrowser.ServerApplication/ApplicationHost.cs

@@ -6,7 +6,6 @@ using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Implementations;
 using MediaBrowser.Common.Implementations.ScheduledTasks;
 using MediaBrowser.Common.IO;
-using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller;
@@ -284,9 +283,6 @@ namespace MediaBrowser.ServerApplication
 
             DtoService = new DtoService(Logger, LibraryManager, UserManager, UserDataManager, ItemRepository, ImageProcessor);
             RegisterSingleInstance(DtoService);
-
-            LiveTvManager = new LiveTvManager(ApplicationPaths, FileSystemManager, Logger, ItemRepository, ImageProcessor, UserDataManager, DtoService, UserManager, LibraryManager);
-            RegisterSingleInstance(LiveTvManager);
             progress.Report(15);
 
             var innerProgress = new ActionableProgress<double>();
@@ -295,6 +291,9 @@ namespace MediaBrowser.ServerApplication
             await RegisterMediaEncoder(innerProgress).ConfigureAwait(false);
             progress.Report(90);
 
+            LiveTvManager = new LiveTvManager(ServerConfigurationManager, FileSystemManager, Logger, ItemRepository, ImageProcessor, UserDataManager, DtoService, UserManager, LibraryManager, MediaEncoder);
+            RegisterSingleInstance(LiveTvManager);
+
             var displayPreferencesTask = Task.Run(async () => await ConfigureDisplayPreferencesRepositories().ConfigureAwait(false));
             var itemsTask = Task.Run(async () => await ConfigureItemRepositories().ConfigureAwait(false));
             var userdataTask = Task.Run(async () => await ConfigureUserDataRepositories().ConfigureAwait(false));

+ 1 - 0
MediaBrowser.WebDashboard/Api/DashboardService.cs

@@ -505,6 +505,7 @@ namespace MediaBrowser.WebDashboard.Api
                                       "livetvtimer.js",
                                       "livetvseriestimer.js",
                                       "livetvseriestimers.js",
+                                      "livetvsettings.js",
                                       "livetvsuggested.js",
                                       "livetvtimers.js",
                                       "loginpage.js",

+ 12 - 1
MediaBrowser.WebDashboard/ApiClient.js

@@ -438,7 +438,7 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi
 
             options = options || {};
             
-            if (options.channelIds) {
+            if (options.channelIds && options.channelIds.length > 1800) {
 
                 return self.ajax({
                     type: "POST",
@@ -458,6 +458,17 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi
             }
         };
 
+        self.getLiveTvRecommendedPrograms = function (options) {
+
+            options = options || {};
+
+            return self.ajax({
+                type: "GET",
+                url: self.getUrl("LiveTv/Programs/Recommended", options),
+                dataType: "json"
+            });
+        };
+
         self.getLiveTvRecordings = function (options) {
 
             var url = self.getUrl("LiveTv/Recordings", options || {});

+ 6 - 0
MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

@@ -184,6 +184,9 @@
     <Content Include="dashboard-ui\livetvseriestimer.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="dashboard-ui\livetvsettings.html">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\livetvtimers.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
@@ -433,6 +436,9 @@
     <Content Include="dashboard-ui\scripts\livetvseriestimer.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="dashboard-ui\scripts\livetvsettings.js">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\scripts\livetvtimer.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>

+ 1 - 1
MediaBrowser.WebDashboard/packages.config

@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
-  <package id="MediaBrowser.ApiClient.Javascript" version="3.0.224" targetFramework="net45" />
+  <package id="MediaBrowser.ApiClient.Javascript" version="3.0.226" targetFramework="net45" />
 </packages>

+ 2 - 2
Nuget/MediaBrowser.Common.Internal.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Common.Internal</id>
-        <version>3.0.298</version>
+        <version>3.0.299</version>
         <title>MediaBrowser.Common.Internal</title>
         <authors>Luke</authors>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.298" />
+            <dependency id="MediaBrowser.Common" version="3.0.299" />
             <dependency id="NLog" version="2.1.0" />
             <dependency id="SimpleInjector" version="2.4.0" />
             <dependency id="sharpcompress" version="0.10.2" />

+ 1 - 1
Nuget/MediaBrowser.Common.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Common</id>
-        <version>3.0.298</version>
+        <version>3.0.299</version>
         <title>MediaBrowser.Common</title>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>

+ 2 - 2
Nuget/MediaBrowser.Server.Core.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Server.Core</id>
-        <version>3.0.298</version>
+        <version>3.0.299</version>
         <title>Media Browser.Server.Core</title>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains core components required to build plugins for Media Browser Server.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.298" />
+            <dependency id="MediaBrowser.Common" version="3.0.299" />
         </dependencies>
     </metadata>
     <files>