diff options
58 files changed, 976 insertions, 313 deletions
diff --git a/MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs b/MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs index 8f9babd06..6cecbd0b6 100644 --- a/MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs +++ b/MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs @@ -5,7 +5,6 @@ using MediaBrowser.Model.Logging; using ServiceStack.Web; using System; using System.Collections.Generic; -using System.Net.Http.Headers; namespace MediaBrowser.Api { @@ -82,6 +81,14 @@ namespace MediaBrowser.Api return GetAuthorization(auth); } + public static User GetCurrentUser(IRequest httpReq, IUserManager userManager) + { + var info = GetAuthorization(httpReq); + + return string.IsNullOrEmpty(info.UserId) ? null : + userManager.GetUserById(new Guid(info.UserId)); + } + /// <summary> /// Gets the authorization. /// </summary> diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index 62fcbd280..556f3b57d 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -1,13 +1,12 @@ -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.Linq; -using ServiceStack.Web; namespace MediaBrowser.Api { @@ -52,11 +51,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> diff --git a/MediaBrowser.Api/Library/LibraryHelpers.cs b/MediaBrowser.Api/Library/LibraryHelpers.cs index e40cb7dd4..3836a0860 100644 --- a/MediaBrowser.Api/Library/LibraryHelpers.cs +++ b/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; - } } } diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index 50e7319b9..5b123eb97 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/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> @@ -258,6 +275,21 @@ namespace MediaBrowser.Api.LiveTv _userManager = userManager; } + private void AssertUserCanManageLiveTv() + { + var user = AuthorizationRequestFilterAttribute.GetCurrentUser(Request, _userManager); + + if (user == null) + { + throw new UnauthorizedAccessException("Anonymous live tv management is not allowed."); + } + + if (!user.Configuration.EnableLiveTvManagement) + { + throw new UnauthorizedAccessException("The current user does not have permission to manage live tv."); + } + } + public object Get(GetServices request) { var services = _liveTvManager.Services @@ -331,6 +363,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); @@ -383,6 +430,8 @@ namespace MediaBrowser.Api.LiveTv public void Delete(DeleteRecording request) { + AssertUserCanManageLiveTv(); + var task = _liveTvManager.DeleteRecording(request.Id); Task.WaitAll(task); @@ -390,6 +439,8 @@ namespace MediaBrowser.Api.LiveTv public void Delete(CancelTimer request) { + AssertUserCanManageLiveTv(); + var task = _liveTvManager.CancelTimer(request.Id); Task.WaitAll(task); @@ -397,6 +448,8 @@ namespace MediaBrowser.Api.LiveTv public void Post(UpdateTimer request) { + AssertUserCanManageLiveTv(); + var task = _liveTvManager.UpdateTimer(request, CancellationToken.None); Task.WaitAll(task); @@ -423,6 +476,8 @@ namespace MediaBrowser.Api.LiveTv public void Delete(CancelSeriesTimer request) { + AssertUserCanManageLiveTv(); + var task = _liveTvManager.CancelSeriesTimer(request.Id); Task.WaitAll(task); @@ -430,6 +485,8 @@ namespace MediaBrowser.Api.LiveTv public void Post(UpdateSeriesTimer request) { + AssertUserCanManageLiveTv(); + var task = _liveTvManager.UpdateSeriesTimer(request, CancellationToken.None); Task.WaitAll(task); @@ -462,6 +519,8 @@ namespace MediaBrowser.Api.LiveTv public void Post(CreateSeriesTimer request) { + AssertUserCanManageLiveTv(); + var task = _liveTvManager.CreateSeriesTimer(request, CancellationToken.None); Task.WaitAll(task); @@ -469,6 +528,8 @@ namespace MediaBrowser.Api.LiveTv public void Post(CreateTimer request) { + AssertUserCanManageLiveTv(); + var task = _liveTvManager.CreateTimer(request, CancellationToken.None); Task.WaitAll(task); diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index 409d152f7..a04aca2a0 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -99,7 +99,6 @@ <Compile Include="Playback\EndlessStreamCopy.cs" /> <Compile Include="Playback\Hls\AudioHlsService.cs" /> <Compile Include="Playback\Hls\BaseHlsService.cs" /> - <Compile Include="Playback\Hls\HlsSegmentResponseFilter.cs" /> <Compile Include="Playback\Hls\HlsSegmentService.cs" /> <Compile Include="Playback\Hls\VideoHlsService.cs" /> <Compile Include="Playback\Progressive\AudioService.cs" /> diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index d9bd873cd..a39e4cf58 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/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; @@ -387,6 +386,7 @@ namespace MediaBrowser.Api.Playback var assSubtitleParam = string.Empty; var copyTsParam = string.Empty; + var yadifParam = "yadif=0:-1:0,"; var request = state.VideoRequest; @@ -408,7 +408,7 @@ namespace MediaBrowser.Api.Playback var widthParam = request.Width.Value.ToString(UsCulture); var heightParam = request.Height.Value.ToString(UsCulture); - return string.Format("{3} -vf \"scale=trunc({0}/2)*2:trunc({1}/2)*2{2}\"", widthParam, heightParam, assSubtitleParam, copyTsParam); + return string.Format("{4} -vf \"{0}scale=trunc({1}/2)*2:trunc({2}/2)*2{3}\"",yadifParam, widthParam, heightParam, assSubtitleParam, copyTsParam); } var isH264Output = outputVideoCodec.Equals("libx264", StringComparison.OrdinalIgnoreCase); @@ -419,8 +419,8 @@ namespace MediaBrowser.Api.Playback var widthParam = request.Width.Value.ToString(UsCulture); return isH264Output ? - string.Format("{2} -vf \"scale={0}:trunc(ow/a/2)*2{1}\"", widthParam, assSubtitleParam, copyTsParam) : - string.Format("{2} -vf \"scale={0}:-1{1}\"", widthParam, assSubtitleParam, copyTsParam); + string.Format("{3} -vf \"{0}scale={1}:trunc(ow/a/2)*2{2}\"",yadifParam, widthParam, assSubtitleParam, copyTsParam) : + string.Format("{3} -vf \"{0}scale={1}:-1{2}\"",yadifParam, widthParam, assSubtitleParam, copyTsParam); } // If a fixed height was requested @@ -429,8 +429,8 @@ namespace MediaBrowser.Api.Playback var heightParam = request.Height.Value.ToString(UsCulture); return isH264Output ? - string.Format("{2} -vf \"scale=trunc(oh*a*2)/2:{0}{1}\"", heightParam, assSubtitleParam, copyTsParam) : - string.Format("{2} -vf \"scale=-1:{0}{1}\"", heightParam, assSubtitleParam, copyTsParam); + string.Format("{3} -vf \"{0}scale=trunc(oh*a*2)/2:{1}{2}\"",yadifParam, heightParam, assSubtitleParam, copyTsParam) : + string.Format("{3} -vf \"{0}scale=-1:{1}{2}\"",yadifParam, heightParam, assSubtitleParam, copyTsParam); } // If a max width was requested @@ -439,8 +439,8 @@ namespace MediaBrowser.Api.Playback var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture); return isH264Output ? - string.Format("{2} -vf \"scale=min(iw\\,{0}):trunc(ow/a/2)*2{1}\"", maxWidthParam, assSubtitleParam, copyTsParam) : - string.Format("{2} -vf \"scale=min(iw\\,{0}):-1{1}\"", maxWidthParam, assSubtitleParam, copyTsParam); + string.Format("{3} -vf \"{0}scale=min(iw\\,{1}):trunc(ow/a/2)*2{2}\"",yadifParam, maxWidthParam, assSubtitleParam, copyTsParam) : + string.Format("{3} -vf \"{0}scale=min(iw\\,{1}):-1{2}\"",yadifParam, maxWidthParam, assSubtitleParam, copyTsParam); } // If a max height was requested @@ -449,8 +449,8 @@ namespace MediaBrowser.Api.Playback var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture); return isH264Output ? - string.Format("{2} -vf \"scale=trunc(oh*a*2)/2:min(ih\\,{0}){1}\"", maxHeightParam, assSubtitleParam, copyTsParam) : - string.Format("{2} -vf \"scale=-1:min(ih\\,{0}){1}\"", maxHeightParam, assSubtitleParam, copyTsParam); + string.Format("{3} -vf \"{0}scale=trunc(oh*a*2)/2:min(ih\\,{1}){2}\"",yadifParam, maxHeightParam, assSubtitleParam, copyTsParam) : + string.Format("{3} -vf \"{0}scale=-1:min(ih\\,{1}){2}\"",yadifParam, maxHeightParam, assSubtitleParam, copyTsParam); } if (state.VideoStream == null) @@ -473,11 +473,11 @@ namespace MediaBrowser.Api.Playback var widthParam = outputSize.Width.ToString(UsCulture); var heightParam = outputSize.Height.ToString(UsCulture); - return string.Format("{3} -vf \"scale=trunc({0}/2)*2:trunc({1}/2)*2{2}\"", widthParam, heightParam, assSubtitleParam, copyTsParam); + return string.Format("{4} -vf \"{0}scale=trunc({1}/2)*2:trunc({2}/2)*2{3}\"",yadifParam, widthParam, heightParam, assSubtitleParam, copyTsParam); } // Otherwise use -vf scale since ffmpeg will ensure internally that the aspect ratio is preserved - return string.Format("{2} -vf \"scale={0}:-1{1}\"", Convert.ToInt32(outputSize.Width), assSubtitleParam, copyTsParam); + return string.Format("{3} -vf \"{0}scale={1}:-1{2}\"",yadifParam, Convert.ToInt32(outputSize.Width), assSubtitleParam, copyTsParam); } /// <summary> @@ -733,7 +733,7 @@ namespace MediaBrowser.Api.Playback return "-"; } - var type = InputType.AudioFile; + var type = InputType.File; var inputPath = new[] { state.MediaPath }; @@ -1043,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)) @@ -1091,6 +1092,7 @@ namespace MediaBrowser.Api.Playback : video.PlayableStreamFileNames.ToList(); } + state.RunTimeTicks = item.RunTimeTicks; itemId = item.Id; } diff --git a/MediaBrowser.Api/Playback/Hls/AudioHlsService.cs b/MediaBrowser.Api/Playback/Hls/AudioHlsService.cs index d5bf22362..a64cdb119 100644 --- a/MediaBrowser.Api/Playback/Hls/AudioHlsService.cs +++ b/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; diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index 8ac8dc9fc..f244886dc 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/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; } diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentResponseFilter.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentResponseFilter.cs deleted file mode 100644 index 10ea6bc63..000000000 --- a/MediaBrowser.Api/Playback/Hls/HlsSegmentResponseFilter.cs +++ /dev/null @@ -1,53 +0,0 @@ -using MediaBrowser.Controller; -using MediaBrowser.Model.Logging; -using ServiceStack.Text.Controller; -using ServiceStack.Web; -using System; -using System.IO; -using System.Linq; - -namespace MediaBrowser.Api.Playback.Hls -{ - public class HlsSegmentResponseFilter : Attribute, IHasResponseFilter - { - public ILogger Logger { get; set; } - public IServerApplicationPaths ApplicationPaths { get; set; } - - public void ResponseFilter(IRequest req, IResponse res, object response) - { - var pathInfo = PathInfo.Parse(req.PathInfo); - var itemId = pathInfo.GetArgumentValue<string>(1); - var playlistId = pathInfo.GetArgumentValue<string>(3); - - OnEndRequest(itemId, playlistId); - } - - public IHasResponseFilter Copy() - { - return this; - } - - public int Priority - { - get { return -1; } - } - - /// <summary> - /// Called when [end request]. - /// </summary> - /// <param name="itemId">The item id.</param> - /// <param name="playlistId">The playlist id.</param> - protected void OnEndRequest(string itemId, string playlistId) - { - Logger.Info("OnEndRequest " + playlistId); - var normalizedPlaylistId = playlistId.Replace("-low", string.Empty); - - foreach (var playlist in Directory.EnumerateFiles(ApplicationPaths.TranscodingTempPath, "*.m3u8") - .Where(i => i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) - .ToList()) - { - ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls); - } - } - } -} diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs index 31583ac19..5d2dd07c7 100644 --- a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs +++ b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs @@ -31,28 +31,6 @@ namespace MediaBrowser.Api.Playback.Hls /// <summary> /// Class GetHlsVideoSegment /// </summary> - [Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.ts", "GET")] - [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] - public class GetHlsVideoSegment - { - /// <summary> - /// Gets or sets the id. - /// </summary> - /// <value>The id.</value> - public string Id { get; set; } - - public string PlaylistId { get; set; } - - /// <summary> - /// Gets or sets the segment id. - /// </summary> - /// <value>The segment id.</value> - public string SegmentId { 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 @@ -104,22 +82,6 @@ namespace MediaBrowser.Api.Playback.Hls /// </summary> /// <param name="request">The request.</param> /// <returns>System.Object.</returns> - public object Get(GetHlsVideoSegment request) - { - var file = request.SegmentId + Path.GetExtension(Request.PathInfo); - - file = Path.Combine(_appPaths.TranscodingTempPath, file); - - OnBeginRequest(request.PlaylistId); - - return ResultFactory.GetStaticFileResult(Request, file); - } - - /// <summary> - /// Gets the specified request. - /// </summary> - /// <param name="request">The request.</param> - /// <returns>System.Object.</returns> public object Get(GetHlsAudioSegment request) { var file = request.SegmentId + Path.GetExtension(Request.PathInfo); diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index 8fbb42f24..174cbe9ba 100644 --- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs @@ -1,13 +1,20 @@ 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.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace MediaBrowser.Api.Playback.Hls { @@ -28,6 +35,51 @@ 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 GetHlsVideoSegment + /// </summary> + [Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.ts", "GET")] + [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] + public class GetHlsVideoSegment + { + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + public string Id { get; set; } + + public string PlaylistId { get; set; } + + /// <summary> + /// Gets or sets the segment id. + /// </summary> + /// <value>The segment id.</value> + public string SegmentId { get; set; } + } + /// <summary> /// Class VideoHlsService /// </summary> @@ -38,6 +90,144 @@ 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; + } + + /// <summary> + /// Gets the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> + public object Get(GetHlsVideoSegment request) + { + var file = request.SegmentId + Path.GetExtension(Request.PathInfo); + + file = Path.Combine(ServerConfigurationManager.ApplicationPaths.TranscodingTempPath, file); + + OnBeginRequest(request.PlaylistId); + + return ResultFactory.GetStaticFileResult(Request, file); + } + + 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> @@ -163,5 +353,30 @@ namespace MediaBrowser.Api.Playback.Hls { return ".ts"; } + + /// <summary> + /// Called when [begin request]. + /// </summary> + /// <param name="playlistId">The playlist id.</param> + protected void OnBeginRequest(string playlistId) + { + var normalizedPlaylistId = playlistId.Replace("-low", string.Empty); + + foreach (var playlist in Directory.EnumerateFiles(ServerConfigurationManager.ApplicationPaths.TranscodingTempPath, "*.m3u8") + .Where(i => i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) + .ToList()) + { + ExtendPlaylistTimer(playlist); + } + } + + private async void ExtendPlaylistTimer(string playlist) + { + ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls); + + await Task.Delay(20000).ConfigureAwait(false); + + ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls); + } } } diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs index 050c06627..a7ef684a5 100644 --- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs +++ b/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; diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index 0ae96effd..9c5acb7a0 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/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; diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index c3ec02a59..f3ccaa244 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/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; diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index bf584c385..17a830380 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/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; } } diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 6098c4bb3..63819c5a9 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/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" /> diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 028fc964d..9e4129cd1 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/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) { diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index d23c8c555..31c336932 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/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); } } diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs index 1e535139c..004f0b452 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs +++ b/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> @@ -118,11 +119,12 @@ namespace MediaBrowser.Controller.LiveTv Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken); /// <summary> - /// Gets the timer defaults asynchronous. + /// Gets the new timer defaults asynchronous. /// </summary> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{TimerInfo}.</returns> - Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken); + /// <param name="program">The program.</param> + /// <returns>Task{SeriesTimerInfo}.</returns> + Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program = null); /// <summary> /// Gets the series timers asynchronous. @@ -130,14 +132,16 @@ namespace MediaBrowser.Controller.LiveTv /// <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 +166,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); } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index abacc0c18..aceb32885 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/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"; diff --git a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs index 6a0d135c8..bf453ccf4 100644 --- a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs +++ b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs @@ -103,6 +103,12 @@ namespace MediaBrowser.Controller.LiveTv public ProgramAudio? Audio { get; set; } /// <summary> + /// Gets or sets the original air date. + /// </summary> + /// <value>The original air date.</value> + public DateTime? OriginalAirDate { get; set; } + + /// <summary> /// Gets or sets a value indicating whether this instance is movie. /// </summary> /// <value><c>true</c> if this instance is movie; otherwise, <c>false</c>.</value> diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 61de32e41..14205e668 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/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" /> @@ -208,6 +210,7 @@ <Compile Include="Sorting\IBaseItemComparer.cs" /> <Compile Include="Sorting\IUserBaseItemComparer.cs" /> <Compile Include="Providers\BaseItemXmlParser.cs" /> + <Compile Include="Sorting\SortExtensions.cs" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj"> diff --git a/MediaBrowser.Controller/MediaInfo/FFMpegManager.cs b/MediaBrowser.Controller/MediaInfo/FFMpegManager.cs index 644222949..c1951038c 100644 --- a/MediaBrowser.Controller/MediaInfo/FFMpegManager.cs +++ b/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; } diff --git a/MediaBrowser.Common/MediaInfo/IMediaEncoder.cs b/MediaBrowser.Controller/MediaInfo/IMediaEncoder.cs index 82643779b..8e0d696c9 100644 --- a/MediaBrowser.Common/MediaInfo/IMediaEncoder.cs +++ b/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> diff --git a/MediaBrowser.Common/MediaInfo/MediaInfoResult.cs b/MediaBrowser.Controller/MediaInfo/InternalMediaInfoResult.cs index d9a666f30..3ceec1b90 100644 --- a/MediaBrowser.Common/MediaInfo/MediaInfoResult.cs +++ b/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. diff --git a/MediaBrowser.Controller/MediaInfo/MediaEncoderHelpers.cs b/MediaBrowser.Controller/MediaInfo/MediaEncoderHelpers.cs index 261454f6d..300071b7b 100644 --- a/MediaBrowser.Controller/MediaInfo/MediaEncoderHelpers.cs +++ b/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"); diff --git a/MediaBrowser.Controller/Sorting/SortExtensions.cs b/MediaBrowser.Controller/Sorting/SortExtensions.cs new file mode 100644 index 000000000..ec8ee5a11 --- /dev/null +++ b/MediaBrowser.Controller/Sorting/SortExtensions.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MediaBrowser.Controller.Sorting +{ + public static class SortExtensions + { + public static IEnumerable<T> OrderByString<T>(this IEnumerable<T> list, Func<T, string> getName) + { + return list.OrderBy(getName, new AlphanumComparator()); + } + + public static IEnumerable<T> OrderByStringDescending<T>(this IEnumerable<T> list, Func<T, string> getName) + { + return list.OrderByDescending(getName, new AlphanumComparator()); + } + + public static IOrderedEnumerable<T> ThenByString<T>(this IOrderedEnumerable<T> list, Func<T, string> getName) + { + return list.ThenBy(getName, new AlphanumComparator()); + } + + public static IOrderedEnumerable<T> ThenByStringDescending<T>(this IOrderedEnumerable<T> list, Func<T, string> getName) + { + return list.ThenByDescending(getName, new AlphanumComparator()); + } + + private class AlphanumComparator : IComparer<string> + { + private enum ChunkType { Alphanumeric, Numeric }; + + private static bool InChunk(char ch, char otherCh) + { + var type = ChunkType.Alphanumeric; + + if (char.IsDigit(otherCh)) + { + type = ChunkType.Numeric; + } + + if ((type == ChunkType.Alphanumeric && char.IsDigit(ch)) + || (type == ChunkType.Numeric && !char.IsDigit(ch))) + { + return false; + } + + return true; + } + + public static int CompareValues(string s1, string s2) + { + if (s1 == null || s2 == null) + { + return 0; + } + + int thisMarker = 0, thisNumericChunk = 0; + int thatMarker = 0, thatNumericChunk = 0; + + while ((thisMarker < s1.Length) || (thatMarker < s2.Length)) + { + if (thisMarker >= s1.Length) + { + return -1; + } + else if (thatMarker >= s2.Length) + { + return 1; + } + char thisCh = s1[thisMarker]; + char thatCh = s2[thatMarker]; + + StringBuilder thisChunk = new StringBuilder(); + StringBuilder thatChunk = new StringBuilder(); + + while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || InChunk(thisCh, thisChunk[0]))) + { + thisChunk.Append(thisCh); + thisMarker++; + + if (thisMarker < s1.Length) + { + thisCh = s1[thisMarker]; + } + } + + while ((thatMarker < s2.Length) && (thatChunk.Length == 0 || InChunk(thatCh, thatChunk[0]))) + { + thatChunk.Append(thatCh); + thatMarker++; + + if (thatMarker < s2.Length) + { + thatCh = s2[thatMarker]; + } + } + + int result = 0; + // If both chunks contain numeric characters, sort them numerically + if (char.IsDigit(thisChunk[0]) && char.IsDigit(thatChunk[0])) + { + if (!int.TryParse(thisChunk.ToString(), out thisNumericChunk)) + { + return 0; + } + if (!int.TryParse(thatChunk.ToString(), out thatNumericChunk)) + { + return 0; + } + + if (thisNumericChunk < thatNumericChunk) + { + result = -1; + } + + if (thisNumericChunk > thatNumericChunk) + { + result = 1; + } + } + else + { + result = thisChunk.ToString().CompareTo(thatChunk.ToString()); + } + + if (result != 0) + { + return result; + } + } + + return 0; + } + + public int Compare(string x, string y) + { + return CompareValues(x, y); + } + } + } +} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index bdf072b50..f7b4cf373 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/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; } + } } diff --git a/MediaBrowser.Model/Configuration/UserConfiguration.cs b/MediaBrowser.Model/Configuration/UserConfiguration.cs index 90accff94..7cc61e7fd 100644 --- a/MediaBrowser.Model/Configuration/UserConfiguration.cs +++ b/MediaBrowser.Model/Configuration/UserConfiguration.cs @@ -67,6 +67,8 @@ namespace MediaBrowser.Model.Configuration public bool BlockUnratedGames { get; set; } public bool BlockUnratedBooks { get; set; } + public bool EnableLiveTvManagement { get; set; } + /// <summary> /// Initializes a new instance of the <see cref="UserConfiguration" /> class. /// </summary> @@ -75,6 +77,8 @@ namespace MediaBrowser.Model.Configuration IsAdministrator = true; EnableRemoteControlOfOtherUsers = true; BlockNotRated = false; + + EnableLiveTvManagement = true; } } } diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 665e36b88..af43ef200 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/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> diff --git a/MediaBrowser.Model/Dto/BaseItemPerson.cs b/MediaBrowser.Model/Dto/BaseItemPerson.cs index 9fd5e1a8a..1cc3f722d 100644 --- a/MediaBrowser.Model/Dto/BaseItemPerson.cs +++ b/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> diff --git a/MediaBrowser.Model/Dto/ChapterInfoDto.cs b/MediaBrowser.Model/Dto/ChapterInfoDto.cs index caa609a76..5a72110ce 100644 --- a/MediaBrowser.Model/Dto/ChapterInfoDto.cs +++ b/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> diff --git a/MediaBrowser.Model/Dto/StudioDto.cs b/MediaBrowser.Model/Dto/StudioDto.cs index 9e2dea85d..696213a40 100644 --- a/MediaBrowser.Model/Dto/StudioDto.cs +++ b/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> diff --git a/MediaBrowser.Model/Dto/UserDto.cs b/MediaBrowser.Model/Dto/UserDto.cs index a79ffc08c..dcf0843fe 100644 --- a/MediaBrowser.Model/Dto/UserDto.cs +++ b/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> diff --git a/MediaBrowser.Model/Entities/BaseItemInfo.cs b/MediaBrowser.Model/Entities/BaseItemInfo.cs index faf6fd47a..49f3e2d8f 100644 --- a/MediaBrowser.Model/Entities/BaseItemInfo.cs +++ b/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> diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 133631649..a8f751c10 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/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>(); + } + } } diff --git a/MediaBrowser.Model/LiveTv/ProgramQuery.cs b/MediaBrowser.Model/LiveTv/ProgramQuery.cs index 36c06d4c0..a2a824994 100644 --- a/MediaBrowser.Model/LiveTv/ProgramQuery.cs +++ b/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; } + } } diff --git a/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs b/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs index 0ef5c9dc0..389df2248 100644 --- a/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs @@ -91,6 +91,12 @@ namespace MediaBrowser.Model.LiveTv public DateTime EndDate { get; set; } /// <summary> + /// Gets or sets the original air date. + /// </summary> + /// <value>The original air date.</value> + public DateTime? OriginalAirDate { get; set; } + + /// <summary> /// Gets or sets the status. /// </summary> /// <value>The status.</value> diff --git a/MediaBrowser.Model/Session/SessionInfoDto.cs b/MediaBrowser.Model/Session/SessionInfoDto.cs index 50dd4da67..083d1ef79 100644 --- a/MediaBrowser.Model/Session/SessionInfoDto.cs +++ b/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> diff --git a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs index fd78a7565..8d768a6a0 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs +++ b/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 { diff --git a/MediaBrowser.Providers/MediaInfo/BaseFFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/BaseFFProbeProvider.cs index cd08f5828..95cbb3e4e 100644 --- a/MediaBrowser.Providers/MediaInfo/BaseFFProbeProvider.cs +++ b/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) { diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfoProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfoProvider.cs index a8432969b..d27b65e2a 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfoProvider.cs +++ b/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); diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfoProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfoProvider.cs index 4ce8cf069..fc2c5c370 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfoProvider.cs +++ b/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) { diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs index d5815690f..fc8826b61 100644 --- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs +++ b/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; } diff --git a/MediaBrowser.Providers/Movies/OpenMovieDatabaseProvider.cs b/MediaBrowser.Providers/Movies/OpenMovieDatabaseProvider.cs index 8940f1d49..306956c95 100644 --- a/MediaBrowser.Providers/Movies/OpenMovieDatabaseProvider.cs +++ b/MediaBrowser.Providers/Movies/OpenMovieDatabaseProvider.cs @@ -172,6 +172,11 @@ namespace MediaBrowser.Providers.Movies item.CommunityRating = imdbRating; } + if (!string.IsNullOrEmpty(result.Website)) + { + item.HomePageUrl = result.Website; + } + ParseAdditionalMetadata(item, result); } @@ -251,6 +256,11 @@ namespace MediaBrowser.Providers.Movies public string Production { get; set; } public string Website { get; set; } public string Response { get; set; } + + public string Language { get; set; } + public string Country { get; set; } + public string Awards { get; set; } + public string Metascore { get; set; } } } } diff --git a/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs b/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs index a4e6f18bb..a8774f1b7 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs +++ b/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> diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index 121bf53d4..55a485318 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -70,7 +70,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV } // Without these movies that have the name season in them could cause the parent folder to be resolved as a series - if (filename.IndexOf("[boxset]", StringComparison.OrdinalIgnoreCase) != -1 || filename.IndexOf("[tmdbid=", StringComparison.OrdinalIgnoreCase) != -1) + if (filename.IndexOf("[tmdbid=", StringComparison.OrdinalIgnoreCase) != -1) { return null; } diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs index 83da28b8f..ad9f769eb 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -161,7 +161,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv return null; } - return val.Value * 2; + return val.Value; } public string GetStatusName(RecordingStatus status) @@ -222,6 +222,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv IsPremiere = info.IsPremiere, RunTimeTicks = (info.EndDate - info.StartDate).Ticks, LocationType = recording.LocationType, + OriginalAirDate = info.OriginalAirDate, MediaStreams = _itemRepo.GetMediaStreams(new MediaStreamQuery { @@ -368,7 +369,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) { diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs index 3bc146bd4..b53b3b651 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs @@ -1,17 +1,21 @@ 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.Controller.Sorting; 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 +27,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 +191,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 +207,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 +432,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv } var returnArray = programs - .OrderBy(i => i.ProgramInfo.StartDate) .Select(i => { var channel = GetChannel(i); @@ -429,6 +453,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 +661,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 +670,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 +699,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); @@ -779,14 +957,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv if (string.Equals(query.SortBy, "Priority", StringComparison.OrdinalIgnoreCase)) { timers = query.SortOrder == SortOrder.Descending ? - timers.OrderBy(i => i.Priority).ThenByDescending(i => i.Name) : - timers.OrderByDescending(i => i.Priority).ThenBy(i => i.Name); + timers.OrderBy(i => i.Priority).ThenByStringDescending(i => i.Name) : + timers.OrderByDescending(i => i.Priority).ThenByString(i => i.Name); } else { timers = query.SortOrder == SortOrder.Descending ? - timers.OrderByDescending(i => i.Name) : - timers.OrderBy(i => i.Name); + timers.OrderByStringDescending(i => i.Name) : + timers.OrderByString(i => i.Name); } var returnArray = timers @@ -833,24 +1011,31 @@ namespace MediaBrowser.Server.Implementations.LiveTv .FirstOrDefault(); } - public async Task<SeriesTimerInfoDto> GetNewTimerDefaults(CancellationToken cancellationToken) + private async Task<SeriesTimerInfo> GetNewTimerDefaultsInternal(CancellationToken cancellationToken, ProgramInfo program = null) { - var service = ActiveService; + var info = await ActiveService.GetNewTimerDefaultsAsync(cancellationToken, program).ConfigureAwait(false); + + info.Id = null; - var info = await service.GetNewTimerDefaultsAsync(cancellationToken).ConfigureAwait(false); + return info; + } - var obj = _tvDtoService.GetSeriesTimerInfoDto(info, service, null); + public async Task<SeriesTimerInfoDto> GetNewTimerDefaults(CancellationToken cancellationToken) + { + var info = await GetNewTimerDefaultsInternal(cancellationToken).ConfigureAwait(false); - obj.Id = obj.ExternalId = string.Empty; + var obj = _tvDtoService.GetSeriesTimerInfoDto(info, ActiveService, null); return obj; } public async Task<SeriesTimerInfoDto> GetNewTimerDefaults(string programId, CancellationToken cancellationToken) { - var info = await GetNewTimerDefaults(cancellationToken).ConfigureAwait(false); + var program = GetInternalProgram(programId).ProgramInfo; + var programDto = await GetProgram(programId, cancellationToken).ConfigureAwait(false); - var program = await GetProgram(programId, cancellationToken).ConfigureAwait(false); + var defaults = await GetNewTimerDefaultsInternal(cancellationToken, program).ConfigureAwait(false); + var info = _tvDtoService.GetSeriesTimerInfoDto(defaults, ActiveService, null); info.Days = new List<DayOfWeek> { @@ -861,13 +1046,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv info.Name = program.Name; info.ChannelId = program.ChannelId; - info.ChannelName = program.ChannelName; + info.ChannelName = programDto.ChannelName; info.EndDate = program.EndDate; info.StartDate = program.StartDate; info.Name = program.Name; info.Overview = program.Overview; info.ProgramId = program.Id; - info.ExternalProgramId = program.ExternalId; + info.ExternalProgramId = programDto.ExternalId; return info; } @@ -977,7 +1162,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv .ToLookup(i => i.Name, StringComparer.OrdinalIgnoreCase) .ToList(); - groups.AddRange(series.OrderBy(i => i.Key).Select(i => new RecordingGroupDto + groups.AddRange(series.OrderByString(i => i.Key).Select(i => new RecordingGroupDto { Name = i.Key, RecordingCount = i.Count() @@ -1047,5 +1232,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(); + } + } + } } } diff --git a/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs b/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs index bcc857a80..b09ff623e 100644 --- a/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs +++ b/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) { @@ -250,11 +249,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder { process.Kill(); } - catch (InvalidOperationException ex1) - { - _logger.ErrorException("Error killing ffprobe", ex1); - } - catch (Win32Exception ex1) + catch (Exception ex1) { _logger.ErrorException("Error killing ffprobe", ex1); } @@ -307,7 +302,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 +792,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 { diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index a14ffc433..27f874cb0 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/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)); diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 9d3e3e468..93bf768e6 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -474,7 +474,6 @@ namespace MediaBrowser.WebDashboard.Api "advancedconfigurationpage.js", "advancedserversettings.js", "metadataadvanced.js", - "boxsets.js", "appsplayback.js", "appsweather.js", "dashboardpage.js", @@ -505,6 +504,7 @@ namespace MediaBrowser.WebDashboard.Api "livetvtimer.js", "livetvseriestimer.js", "livetvseriestimers.js", + "livetvsettings.js", "livetvsuggested.js", "livetvtimers.js", "loginpage.js", @@ -514,6 +514,7 @@ namespace MediaBrowser.WebDashboard.Api "metadataconfigurationpage.js", "metadataimagespage.js", "moviegenres.js", + "moviecollections.js", "movies.js", "moviepeople.js", "moviesrecommended.js", diff --git a/MediaBrowser.WebDashboard/ApiClient.js b/MediaBrowser.WebDashboard/ApiClient.js index fe0e5e541..578f71176 100644 --- a/MediaBrowser.WebDashboard/ApiClient.js +++ b/MediaBrowser.WebDashboard/ApiClient.js @@ -378,6 +378,17 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi }); }; + self.getAuthorizedFeatures = function (options) { + + var url = self.getUrl("Users/AuthorizedFeatures", options || {}); + + return self.ajax({ + type: "GET", + url: url, + dataType: "json" + }); + }; + self.getLiveTvServices = function (options) { var url = self.getUrl("LiveTv/Services", options || {}); @@ -438,7 +449,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 +469,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 || {}); diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index 50113c19f..78164a5af 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/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>
@@ -1228,7 +1234,7 @@ <Content Include="dashboard-ui\playlist.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
- <Content Include="dashboard-ui\scripts\boxsets.js">
+ <Content Include="dashboard-ui\scripts\moviecollections.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\scripts\gamegenrepage.js">
diff --git a/MediaBrowser.WebDashboard/packages.config b/MediaBrowser.WebDashboard/packages.config index 6c512e8bb..939ca9997 100644 --- a/MediaBrowser.WebDashboard/packages.config +++ b/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.228" targetFramework="net45" /> </packages>
\ No newline at end of file diff --git a/Nuget/MediaBrowser.Common.Internal.nuspec b/Nuget/MediaBrowser.Common.Internal.nuspec index 534957de6..dd0a06615 100644 --- a/Nuget/MediaBrowser.Common.Internal.nuspec +++ b/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.300</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.300" /> <dependency id="NLog" version="2.1.0" /> <dependency id="SimpleInjector" version="2.4.0" /> <dependency id="sharpcompress" version="0.10.2" /> diff --git a/Nuget/MediaBrowser.Common.nuspec b/Nuget/MediaBrowser.Common.nuspec index e6792e1df..72fc2ac63 100644 --- a/Nuget/MediaBrowser.Common.nuspec +++ b/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.300</version> <title>MediaBrowser.Common</title> <authors>Media Browser Team</authors> <owners>ebr,Luke,scottisafool</owners> diff --git a/Nuget/MediaBrowser.Server.Core.nuspec b/Nuget/MediaBrowser.Server.Core.nuspec index db48a2891..f5db426ca 100644 --- a/Nuget/MediaBrowser.Server.Core.nuspec +++ b/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.300</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.300" /> </dependencies> </metadata> <files> |
