diff options
36 files changed, 1093 insertions, 379 deletions
diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index 7ccb83bc8..9d827226c 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -106,6 +106,14 @@ namespace MediaBrowser.Api.LiveTv public string UserId { get; set; } } + [Route("/LiveTv/Tuners/{Id}/Reset", "POST")] + [Api(Description = "Resets a tv tuner")] + public class ResetTuner : IReturnVoid + { + [ApiMember(Name = "Id", Description = "Tuner Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + } + [Route("/LiveTv/Timers/{Id}", "GET")] [Api(Description = "Gets a live tv timer")] public class GetTimer : IReturn<TimerInfoDto> @@ -294,20 +302,7 @@ namespace MediaBrowser.Api.LiveTv public object Get(GetLiveTvInfo request) { - var services = _liveTvManager.GetServiceInfos(CancellationToken.None).Result; - var servicesList = services.ToList(); - - var activeServiceInfo = _liveTvManager.ActiveService == null ? null : - servicesList.FirstOrDefault(i => string.Equals(i.Name, _liveTvManager.ActiveService.Name, StringComparison.OrdinalIgnoreCase)); - - var info = new LiveTvInfo - { - Services = servicesList.ToList(), - ActiveServiceName = activeServiceInfo == null ? null : activeServiceInfo.Name, - IsEnabled = _liveTvManager.ActiveService != null, - Status = activeServiceInfo == null ? LiveTvServiceStatus.Unavailable : activeServiceInfo.Status, - StatusMessage = activeServiceInfo == null ? null : activeServiceInfo.StatusMessage - }; + var info = _liveTvManager.GetLiveTvInfo(CancellationToken.None).Result; return ToOptimizedResult(info); } @@ -568,5 +563,14 @@ namespace MediaBrowser.Api.LiveTv { return ToOptimizedResult(_liveTvManager.GetGuideInfo()); } + + public void Post(ResetTuner request) + { + AssertUserCanManageLiveTv(); + + var task = _liveTvManager.ResetTuner(request.Id, CancellationToken.None); + + Task.WaitAll(task); + } } }
\ No newline at end of file diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index f9dfadd1f..f4b68810b 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -101,6 +101,7 @@ <Compile Include="Playback\EndlessStreamCopy.cs" /> <Compile Include="Playback\Hls\AudioHlsService.cs" /> <Compile Include="Playback\Hls\BaseHlsService.cs" /> + <Compile Include="Playback\Hls\DynamicHlsService.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 394ca69d5..76cce0d66 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -307,7 +307,7 @@ namespace MediaBrowser.Api.Playback if (videoCodec.Equals("libvpx", StringComparison.OrdinalIgnoreCase)) { // http://www.webmproject.org/docs/encoder-parameters/ - return "-speed 16 -quality good -profile:v 0 -slices 8"; + return "-speed 16 -quality good -profile:v 0 -slices 8 -crf 18"; } // asf/wmv @@ -321,11 +321,11 @@ namespace MediaBrowser.Api.Playback switch (GetQualitySetting()) { case EncodingQuality.HighSpeed: - return "-preset ultrafast"; + return "-preset ultrafast -crf 18"; case EncodingQuality.HighQuality: - return "-preset superfast"; + return "-preset superfast -crf 18"; case EncodingQuality.MaxQuality: - return "-preset superfast"; + return "-preset superfast -crf 18"; default: throw new Exception("Unrecognized MediaEncodingQuality value."); } @@ -381,7 +381,7 @@ namespace MediaBrowser.Api.Playback audioSampleRate, volParam, pts, - state.AudioSync.ToString(UsCulture)); + state.AudioSync); } /// <summary> @@ -994,6 +994,26 @@ namespace MediaBrowser.Api.Playback } } + protected double? GetFramerateParam(StreamState state) + { + if (state.VideoRequest != null && state.VideoRequest.Framerate.HasValue) + { + return state.VideoRequest.Framerate.Value; + } + + if (state.VideoStream != null) + { + var contentRate = state.VideoStream.AverageFrameRate ?? state.VideoStream.RealFrameRate; + + if (contentRate.HasValue && contentRate.Value > 23.976) + { + return 23.976; + } + } + + return null; + } + /// <summary> /// Gets the state. /// </summary> @@ -1068,7 +1088,7 @@ namespace MediaBrowser.Api.Playback //state.RunTimeTicks = recording.RunTimeTicks; state.ReadInputAtNativeFramerate = recording.RecordingInfo.Status == RecordingStatus.InProgress; state.SendInputOverStandardInput = recording.RecordingInfo.Status == RecordingStatus.InProgress; - state.AudioSync = 1000; + state.AudioSync = "1000"; state.DeInterlace = true; } else if (item is LiveTvChannel) @@ -1096,7 +1116,7 @@ namespace MediaBrowser.Api.Playback state.SendInputOverStandardInput = true; state.ReadInputAtNativeFramerate = true; - state.AudioSync = 1000; + state.AudioSync = "1000"; state.DeInterlace = true; } else diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index b162d582d..736d2122d 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -117,15 +117,7 @@ namespace MediaBrowser.Api.Playback.Hls if (isPlaylistNewlyCreated) { - var minimumSegmentCount = 3; - var quality = GetQualitySetting(); - - if (quality == EncodingQuality.HighSpeed || quality == EncodingQuality.HighQuality) - { - minimumSegmentCount = 2; - } - - await WaitForMinimumSegmentCount(playlist, minimumSegmentCount).ConfigureAwait(false); + await WaitForMinimumSegmentCount(playlist, GetSegmentWait()).ConfigureAwait(false); } int audioBitrate; @@ -155,6 +147,23 @@ namespace MediaBrowser.Api.Playback.Hls } /// <summary> + /// Gets the segment wait. + /// </summary> + /// <returns>System.Int32.</returns> + protected int GetSegmentWait() + { + var minimumSegmentCount = 3; + var quality = GetQualitySetting(); + + if (quality == EncodingQuality.HighSpeed || quality == EncodingQuality.HighQuality) + { + minimumSegmentCount = 2; + } + + return minimumSegmentCount; + } + + /// <summary> /// Gets the playlist bitrates. /// </summary> /// <param name="state">The state.</param> @@ -210,7 +219,7 @@ namespace MediaBrowser.Api.Playback.Hls return builder.ToString(); } - private async Task WaitForMinimumSegmentCount(string playlist, int segmentCount) + protected async Task WaitForMinimumSegmentCount(string playlist, int segmentCount) { while (true) { @@ -273,8 +282,11 @@ namespace MediaBrowser.Api.Playback.Hls var threads = GetNumberOfThreads(false); var inputModifier = GetInputModifier(state); + + // If performSubtitleConversions is true we're actually starting ffmpeg + var startNumberParam = performSubtitleConversions ? GetStartNumber(state).ToString(UsCulture) : "0"; - var args = string.Format("{0} {1} -i {2}{3} -map_metadata -1 -threads {4} {5} {6} -sc_threshold 0 {7} -hls_time {8} -start_number 0 -hls_list_size 1440 \"{9}\"", + var args = string.Format("{0} {1} -i {2}{3} -map_metadata -1 -threads {4} {5} {6} -sc_threshold 0 {7} -hls_time {8} -start_number {9} -hls_list_size 1440 \"{10}\"", itsOffset, inputModifier, GetInputArgument(state), @@ -284,6 +296,7 @@ namespace MediaBrowser.Api.Playback.Hls GetVideoArguments(state, performSubtitleConversions), GetAudioArguments(state), state.SegmentLength.ToString(UsCulture), + startNumberParam, outputPath ).Trim(); @@ -295,10 +308,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 {1} -hls_time {2} -start_number 0 -hls_list_size 1440 \"{3}\"", + var lowBitrateParams = string.Format(" -threads {0} -vn -codec:a:0 libmp3lame -ac 2 -ab {1} -hls_time {2} -start_number {3} -hls_list_size 1440 \"{4}\"", threads, bitrate / 2, state.SegmentLength.ToString(UsCulture), + startNumberParam, lowBitratePath); args += " " + lowBitrateParams; @@ -307,5 +321,10 @@ namespace MediaBrowser.Api.Playback.Hls return args; } + + protected virtual int GetStartNumber(StreamState state) + { + return 0; + } } } diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs new file mode 100644 index 000000000..388339f17 --- /dev/null +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -0,0 +1,360 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.IO; +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.Globalization; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.Playback.Hls +{ + [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}/hlsdynamic/{PlaylistId}/{SegmentId}.ts", "GET")] + [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] + public class GetDynamicHlsVideoSegment : VideoStreamRequest + { + public string PlaylistId { get; set; } + + /// <summary> + /// Gets or sets the segment id. + /// </summary> + /// <value>The segment id.</value> + public string SegmentId { get; set; } + } + + public class DynamicHlsService : BaseHlsService + { + public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository, ILiveTvManager liveTvManager) + : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, dtoService, fileSystem, itemRepository, liveTvManager) + { + } + + protected override string GetOutputFilePath(StreamState state) + { + var folder = (state.MediaPath + state.Request.DeviceId).GetMD5().ToString("N"); + + folder = Path.Combine(ServerConfigurationManager.ApplicationPaths.TranscodingTempPath, folder); + + var outputFileExtension = GetOutputFileExtension(state); + + return Path.Combine(folder, GetCommandLineArguments("dummy\\dummy", state, false).GetMD5() + (outputFileExtension ?? string.Empty).ToLower()); + } + + public object Get(GetMasterHlsVideoStream request) + { + var result = GetAsync(request).Result; + + return result; + } + + public object Get(GetDynamicHlsVideoSegment request) + { + if (string.Equals("baseline", request.PlaylistId, StringComparison.OrdinalIgnoreCase)) + { + return GetDynamicSegment(request, false).Result; + } + + return GetDynamicSegment(request, true).Result; + } + + private async Task<object> GetDynamicSegment(GetDynamicHlsVideoSegment request, bool isMain) + { + var index = int.Parse(request.SegmentId, NumberStyles.Integer, UsCulture); + + var state = await GetState(request, CancellationToken.None).ConfigureAwait(false); + + var playlistPath = Path.ChangeExtension(GetOutputFilePath(state), ".m3u8"); + + var path = GetSegmentPath(playlistPath, index); + + if (File.Exists(path)) + { + return GetSegementResult(path); + } + + if (!File.Exists(playlistPath)) + { + await StartFfMpeg(state, playlistPath).ConfigureAwait(false); + + await WaitForMinimumSegmentCount(playlistPath, GetSegmentWait()).ConfigureAwait(false); + } + + return GetSegementResult(path); + } + + private string GetSegmentPath(string playlist, int index) + { + var folder = Path.GetDirectoryName(playlist); + + var filename = Path.GetFileNameWithoutExtension(playlist); + + return Path.Combine(folder, filename + index.ToString(UsCulture) + ".ts"); + } + + private object GetSegementResult(string path) + { + // TODO: Handle if it's currently being written to + return ResultFactory.GetStaticFileResult(Request, path); + } + + 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(); + } + + public object Get(GetMainHlsVideoStream request) + { + var result = GetPlaylistAsync(request, "main").Result; + + return result; + } + + public object Get(GetBaselineHlsVideoStream request) + { + var result = GetPlaylistAsync(request, "baseline").Result; + + return result; + } + + private async Task<object> GetPlaylistAsync(VideoStreamRequest request, string name) + { + var state = await GetState(request, CancellationToken.None).ConfigureAwait(false); + + var builder = new StringBuilder(); + + builder.AppendLine("#EXTM3U"); + builder.AppendLine("#EXT-X-VERSION:3"); + builder.AppendLine("#EXT-X-TARGETDURATION:" + state.SegmentLength.ToString(UsCulture)); + builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0"); + + var queryStringIndex = Request.RawUrl.IndexOf('?'); + var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex); + + var seconds = TimeSpan.FromTicks(state.RunTimeTicks ?? 0).TotalSeconds; + + var index = 0; + + while (seconds > 0) + { + var length = seconds >= state.SegmentLength ? state.SegmentLength : seconds; + + builder.AppendLine("#EXTINF:" + length.ToString(UsCulture)); + + builder.AppendLine(string.Format("hlsdynamic/{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>()); + } + + protected override string GetAudioArguments(StreamState state) + { + var codec = GetAudioCodec(state.Request); + + if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase)) + { + return "-codec:a:0 copy"; + } + + var args = "-codec:a:0 " + codec; + + if (state.AudioStream != null) + { + var channels = GetNumAudioChannelsParam(state.Request, state.AudioStream); + + if (channels.HasValue) + { + args += " -ac " + channels.Value; + } + + var bitrate = GetAudioBitrateParam(state); + + if (bitrate.HasValue) + { + args += " -ab " + bitrate.Value.ToString(UsCulture); + } + + args += " " + GetAudioFilterParam(state, true); + + return args; + } + + return args; + } + + protected override string GetVideoArguments(StreamState state, bool performSubtitleConversion) + { + var codec = GetVideoCodec(state.VideoRequest); + + // See if we can save come cpu cycles by avoiding encoding + if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase)) + { + return IsH264(state.VideoStream) ? "-codec:v:0 copy -bsf h264_mp4toannexb" : "-codec:v:0 copy"; + } + + const string keyFrameArg = " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+5))"; + + var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsExternal && + (state.SubtitleStream.Codec.IndexOf("pgs", StringComparison.OrdinalIgnoreCase) != -1 || + state.SubtitleStream.Codec.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1); + + var args = "-codec:v:0 " + codec + " " + GetVideoQualityParam(state, "libx264") + keyFrameArg; + + var bitrate = GetVideoBitrateParam(state); + + if (bitrate.HasValue) + { + args += string.Format(" -b:v {0} -maxrate ({0}*.80) -bufsize {0}", bitrate.Value.ToString(UsCulture)); + } + + // Add resolution params, if specified + if (!hasGraphicalSubs) + { + if (state.VideoRequest.Width.HasValue || state.VideoRequest.Height.HasValue || state.VideoRequest.MaxHeight.HasValue || state.VideoRequest.MaxWidth.HasValue) + { + args += GetOutputSizeParam(state, codec, performSubtitleConversion); + } + } + + var framerate = GetFramerateParam(state); + if (framerate.HasValue) + { + args += string.Format(" -r {0}", framerate.Value.ToString(UsCulture)); + } + + if (!string.IsNullOrEmpty(state.VideoSync)) + { + args += " -vsync " + state.VideoSync; + } + + if (!string.IsNullOrEmpty(state.VideoRequest.Profile)) + { + args += " -profile:v " + state.VideoRequest.Profile; + } + + if (!string.IsNullOrEmpty(state.VideoRequest.Level)) + { + args += " -level " + state.VideoRequest.Level; + } + + // This is for internal graphical subs + if (hasGraphicalSubs) + { + args += GetInternalGraphicalSubtitleParam(state, codec); + } + + return args; + } + + /// <summary> + /// Gets the segment file extension. + /// </summary> + /// <param name="state">The state.</param> + /// <returns>System.String.</returns> + protected override string GetSegmentFileExtension(StreamState state) + { + return ".ts"; + } + } +} diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index 174cbe9ba..9d9c62a28 100644 --- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs @@ -5,15 +5,11 @@ 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 @@ -35,35 +31,12 @@ 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 + public class GetHlsVideoSegment : VideoStreamRequest { /// <summary> /// Gets or sets the id. @@ -90,27 +63,6 @@ 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> @@ -127,105 +79,29 @@ namespace MediaBrowser.Api.Playback.Hls 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) + /// <summary> + /// Called when [begin request]. + /// </summary> + /// <param name="playlistId">The playlist id.</param> + protected void OnBeginRequest(string playlistId) { - 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 normalizedPlaylistId = playlistId.Replace("-low", string.Empty); - var hlsVideoRequest = state.VideoRequest as GetMasterHlsVideoStream; - if (hlsVideoRequest != null) + foreach (var playlist in Directory.EnumerateFiles(ServerConfigurationManager.ApplicationPaths.TranscodingTempPath, "*.m3u8") + .Where(i => i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) + .ToList()) { - appendBaselineStream = hlsVideoRequest.AppendBaselineStream; - baselineStreamBitrate = hlsVideoRequest.BaselineStreamAudioBitRate ?? baselineStreamBitrate; + ExtendPlaylistTimer(playlist); } - - 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) + private async void ExtendPlaylistTimer(string playlist) { - 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); + ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls); - // 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); - } + await Task.Delay(20000).ConfigureAwait(false); - return builder.ToString(); + ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls); } /// <summary> @@ -318,12 +194,16 @@ namespace MediaBrowser.Api.Playback.Hls } } - if (state.VideoRequest.Framerate.HasValue) + var framerate = GetFramerateParam(state); + if (framerate.HasValue) { - args += string.Format(" -r {0}", state.VideoRequest.Framerate.Value); + args += string.Format(" -r {0}", framerate.Value.ToString(UsCulture)); } - args += " -vsync vfr"; + if (!string.IsNullOrEmpty(state.VideoSync)) + { + args += " -vsync " + state.VideoSync; + } if (!string.IsNullOrEmpty(state.VideoRequest.Profile)) { @@ -353,30 +233,5 @@ 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/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index 826b03440..b16761595 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -156,9 +156,10 @@ namespace MediaBrowser.Api.Playback.Progressive } } - if (request.Framerate.HasValue) + var framerate = GetFramerateParam(state); + if (framerate.HasValue) { - args += string.Format(" -r {0}", request.Framerate.Value); + args += string.Format(" -r {0}", framerate.Value.ToString(UsCulture)); } var qualityParam = GetVideoQualityParam(state, codec); @@ -169,11 +170,13 @@ namespace MediaBrowser.Api.Playback.Progressive { if (string.Equals(codec, "libvpx", StringComparison.OrdinalIgnoreCase)) { - qualityParam += string.Format(" -minrate:v ({0}*.90) -maxrate:v ({0}*1.10) -bufsize:v {0} -b:v {0}", bitrate.Value.ToString(UsCulture)); + qualityParam += string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture)); } else { - qualityParam += string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture)); + qualityParam += string.Format(" -maxrate {0} -bufsize {1}", + bitrate.Value.ToString(UsCulture), + (bitrate.Value * 2).ToString(UsCulture)); } } @@ -182,7 +185,10 @@ namespace MediaBrowser.Api.Playback.Progressive args += " " + qualityParam.Trim(); } - args += " -vsync vfr"; + if (!string.IsNullOrEmpty(state.VideoSync)) + { + args += " -vsync " + state.VideoSync; + } if (!string.IsNullOrEmpty(state.VideoRequest.Profile)) { diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index 84cc8ecd3..55d7b22e2 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/MediaBrowser.Api/Playback/StreamState.cs @@ -59,7 +59,8 @@ namespace MediaBrowser.Api.Playback public long? RunTimeTicks; - public int AudioSync = 1; + public string AudioSync = "1"; + public string VideoSync = "vfr"; public bool DeInterlace { get; set; } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 58d5f1b94..7840fb3f0 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -542,11 +542,6 @@ namespace MediaBrowser.Controller.Entities public long? RunTimeTicks { get; set; } /// <summary> - /// Gets or sets the original run time ticks. - /// </summary> - /// <value>The original run time ticks.</value> - public long? OriginalRunTimeTicks { get; set; } - /// <summary> /// Gets or sets the production year. /// </summary> /// <value>The production year.</value> diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index 88e2c9b4e..ad42c5091 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -25,13 +25,6 @@ namespace MediaBrowser.Controller.LiveTv IReadOnlyList<ILiveTvService> Services { get; } /// <summary> - /// Gets the service infos. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{IEnumerable{LiveTvServiceInfo}}.</returns> - Task<IEnumerable<LiveTvServiceInfo>> GetServiceInfos(CancellationToken cancellationToken); - - /// <summary> /// Gets the new timer defaults asynchronous. /// </summary> /// <param name="cancellationToken">The cancellation token.</param> @@ -256,5 +249,20 @@ namespace MediaBrowser.Controller.LiveTv /// <returns>Task{QueryResult{ProgramInfoDto}}.</returns> Task<QueryResult<ProgramInfoDto>> GetRecommendedPrograms(RecommendedProgramQuery query, CancellationToken cancellationToken); + + /// <summary> + /// Gets the live tv information. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{LiveTvInfo}.</returns> + Task<LiveTvInfo> GetLiveTvInfo(CancellationToken cancellationToken); + + /// <summary> + /// Resets the tuner. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task ResetTuner(string id, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs index 988b62002..3abbe500f 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs @@ -197,5 +197,13 @@ namespace MediaBrowser.Controller.LiveTv /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> Task RecordLiveStream(string id, CancellationToken cancellationToken); + + /// <summary> + /// Resets the tuner. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task ResetTuner(string id, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs index 1e6d74ce8..f37e94714 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs @@ -25,17 +25,51 @@ namespace MediaBrowser.Controller.LiveTv [IgnoreDataMember] public List<ItemByNameCounts> UserItemCountList { get; set; } - public ChannelInfo ChannelInfo { get; set; } + /// <summary> + /// Gets or sets the number. + /// </summary> + /// <value>The number.</value> + public string Number { get; set; } + + /// <summary> + /// Gets or sets the external identifier. + /// </summary> + /// <value>The external identifier.</value> + public string ExternalId { get; set; } + + /// <summary> + /// Gets or sets the type of the channel. + /// </summary> + /// <value>The type of the channel.</value> + public ChannelType ChannelType { get; set; } public string ServiceName { get; set; } + /// <summary> + /// Supply the image path if it can be accessed directly from the file system + /// </summary> + /// <value>The image path.</value> + public string ProviderImagePath { get; set; } + + /// <summary> + /// Supply the image url if it can be downloaded + /// </summary> + /// <value>The image URL.</value> + public string ProviderImageUrl { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance has image. + /// </summary> + /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value> + public bool? HasProviderImage { get; set; } + protected override string CreateSortName() { double number = 0; - if (!string.IsNullOrEmpty(ChannelInfo.Number)) + if (!string.IsNullOrEmpty(Number)) { - double.TryParse(ChannelInfo.Number, out number); + double.TryParse(Number, out number); } return number.ToString("000-") + (Name ?? string.Empty); @@ -45,7 +79,7 @@ namespace MediaBrowser.Controller.LiveTv { get { - return ChannelInfo.ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video; + return ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video; } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index aceb32885..6a00607e4 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -15,12 +15,118 @@ namespace MediaBrowser.Controller.LiveTv return GetClientTypeName() + "-" + Name; } - public ProgramInfo ProgramInfo { get; set; } + /// <summary> + /// Id of the program. + /// </summary> + public string ExternalId { get; set; } + /// <summary> + /// Gets or sets the channel identifier. + /// </summary> + /// <value>The channel identifier.</value> + public string ExternalChannelId { get; set; } + + /// <summary> + /// Gets or sets the type of the channel. + /// </summary> + /// <value>The type of the channel.</value> public ChannelType ChannelType { get; set; } + /// <summary> + /// The start date of the program, in UTC. + /// </summary> + public DateTime StartDate { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is hd. + /// </summary> + /// <value><c>true</c> if this instance is hd; otherwise, <c>false</c>.</value> + public bool? IsHD { get; set; } + + /// <summary> + /// Gets or sets the audio. + /// </summary> + /// <value>The audio.</value> + public ProgramAudio? Audio { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is repeat. + /// </summary> + /// <value><c>true</c> if this instance is repeat; otherwise, <c>false</c>.</value> + public bool IsRepeat { get; set; } + + /// <summary> + /// Gets or sets the episode title. + /// </summary> + /// <value>The episode title.</value> + public string EpisodeTitle { get; set; } + + /// <summary> + /// Gets or sets the name of the service. + /// </summary> + /// <value>The name of the service.</value> public string ServiceName { get; set; } + /// <summary> + /// Supply the image path if it can be accessed directly from the file system + /// </summary> + /// <value>The image path.</value> + public string ProviderImagePath { get; set; } + + /// <summary> + /// Supply the image url if it can be downloaded + /// </summary> + /// <value>The image URL.</value> + public string ProviderImageUrl { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance has image. + /// </summary> + /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value> + public bool? HasProviderImage { 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> + public bool IsMovie { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is sports. + /// </summary> + /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value> + public bool IsSports { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is series. + /// </summary> + /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value> + public bool IsSeries { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is live. + /// </summary> + /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value> + public bool IsLive { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is news. + /// </summary> + /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value> + public bool IsNews { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is kids. + /// </summary> + /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value> + public bool IsKids { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is premiere. + /// </summary> + /// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value> + public bool IsPremiere { get; set; } + public override string MediaType { get @@ -35,7 +141,7 @@ namespace MediaBrowser.Controller.LiveTv { var now = DateTime.UtcNow; - return now >= ProgramInfo.StartDate && now < ProgramInfo.EndDate; + return now >= StartDate && now < EndDate; } } @@ -45,7 +151,7 @@ namespace MediaBrowser.Controller.LiveTv { var now = DateTime.UtcNow; - return now >= ProgramInfo.EndDate; + return now >= EndDate; } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs index 4159e5271..da6b8ab17 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs @@ -74,12 +74,18 @@ namespace MediaBrowser.Controller.LiveTv public string ChannelId { get; set; } /// <summary> - /// Gets or sets the timer identifier. + /// Gets or sets the recording identifier. /// </summary> - /// <value>The timer identifier.</value> - public string TimerId { get; set; } + /// <value>The recording identifier.</value> + public string RecordingId { get; set; } /// <summary> + /// Gets or sets the name of the program. + /// </summary> + /// <value>The name of the program.</value> + public string ProgramName { get; set; } + + /// <summary> /// Gets or sets the clients. /// </summary> /// <value>The clients.</value> diff --git a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs index 0368c5f2f..4d7e5ee63 100644 --- a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs @@ -18,12 +18,6 @@ namespace MediaBrowser.Controller.LiveTv public string ChannelId { get; set; } /// <summary> - /// Gets or sets the channel primary image tag. - /// </summary> - /// <value>The channel primary image tag.</value> - public Guid? ChannelPrimaryImageTag { get; set; } - - /// <summary> /// Name of the program /// </summary> public string Name { get; set; } @@ -104,6 +98,12 @@ namespace MediaBrowser.Controller.LiveTv public string ImageUrl { get; set; } /// <summary> + /// Gets or sets a value indicating whether this instance has image. + /// </summary> + /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value> + public bool? HasImage { 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> @@ -145,12 +145,6 @@ namespace MediaBrowser.Controller.LiveTv /// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value> public bool IsPremiere { get; set; } - /// <summary> - /// Gets or sets a value indicating whether this instance has image. - /// </summary> - /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value> - public bool? HasImage { get; set; } - public ProgramInfo() { Genres = new List<string>(); diff --git a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs index 416437d35..7bd6d824a 100644 --- a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs +++ b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs @@ -423,11 +423,7 @@ namespace MediaBrowser.Controller.Providers if (int.TryParse(text.Split(' ')[0], NumberStyles.Integer, _usCulture, out runtime)) { // For audio and video don't replace ffmpeg data - if (item is Video || item is Audio) - { - item.OriginalRunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks; - } - else + if (!(item is Video || item is Audio)) { item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks; } diff --git a/MediaBrowser.Model/Configuration/UserConfiguration.cs b/MediaBrowser.Model/Configuration/UserConfiguration.cs index ba4726956..c306c7775 100644 --- a/MediaBrowser.Model/Configuration/UserConfiguration.cs +++ b/MediaBrowser.Model/Configuration/UserConfiguration.cs @@ -68,6 +68,7 @@ namespace MediaBrowser.Model.Configuration public bool BlockUnratedBooks { get; set; } public bool EnableLiveTvManagement { get; set; } + public bool EnableLiveTvAccess { get; set; } public bool EnableMediaPlayback { get; set; } @@ -82,6 +83,7 @@ namespace MediaBrowser.Model.Configuration EnableLiveTvManagement = true; EnableMediaPlayback = true; + EnableLiveTvAccess = true; } } } diff --git a/MediaBrowser.Model/FileOrganization/FileOrganizationResult.cs b/MediaBrowser.Model/FileOrganization/FileOrganizationResult.cs index d2d52f4aa..e01df6d94 100644 --- a/MediaBrowser.Model/FileOrganization/FileOrganizationResult.cs +++ b/MediaBrowser.Model/FileOrganization/FileOrganizationResult.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace MediaBrowser.Model.FileOrganization { @@ -81,6 +82,17 @@ namespace MediaBrowser.Model.FileOrganization /// </summary> /// <value>The type.</value> public FileOrganizerType Type { get; set; } + + /// <summary> + /// Gets or sets the duplicate paths. + /// </summary> + /// <value>The duplicate paths.</value> + public List<string> DuplicatePaths { get; set; } + + public FileOrganizationResult() + { + DuplicatePaths = new List<string>(); + } } public enum FileSortingStatus diff --git a/MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs b/MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs index 8597f6720..85f58be3b 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs @@ -43,6 +43,13 @@ namespace MediaBrowser.Model.LiveTv /// </summary> /// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value> public bool HasUpdateAvailable { get; set; } + + public List<LiveTvTunerInfoDto> Tuners { get; set; } + + public LiveTvServiceInfo() + { + Tuners = new List<LiveTvTunerInfoDto>(); + } } public class GuideInfo @@ -81,6 +88,12 @@ namespace MediaBrowser.Model.LiveTv public bool IsEnabled { get; set; } /// <summary> + /// Gets or sets the enabled users. + /// </summary> + /// <value>The enabled users.</value> + public List<string> EnabledUsers { get; set; } + + /// <summary> /// Gets or sets the status. /// </summary> /// <value>The status.</value> @@ -95,6 +108,69 @@ namespace MediaBrowser.Model.LiveTv public LiveTvInfo() { Services = new List<LiveTvServiceInfo>(); + EnabledUsers = new List<string>(); + } + } + + public class LiveTvTunerInfoDto + { + /// <summary> + /// Gets or sets the type of the source. + /// </summary> + /// <value>The type of the source.</value> + public string SourceType { get; set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + + /// <summary> + /// Gets or sets the identifier. + /// </summary> + /// <value>The identifier.</value> + public string Id { get; set; } + + /// <summary> + /// Gets or sets the status. + /// </summary> + /// <value>The status.</value> + public LiveTvTunerStatus Status { get; set; } + + /// <summary> + /// Gets or sets the channel identifier. + /// </summary> + /// <value>The channel identifier.</value> + public string ChannelId { get; set; } + + /// <summary> + /// Gets or sets the name of the channel. + /// </summary> + /// <value>The name of the channel.</value> + public string ChannelName { get; set; } + + /// <summary> + /// Gets or sets the recording identifier. + /// </summary> + /// <value>The recording identifier.</value> + public string RecordingId { get; set; } + + /// <summary> + /// Gets or sets the name of the program. + /// </summary> + /// <value>The name of the program.</value> + public string ProgramName { get; set; } + + /// <summary> + /// Gets or sets the clients. + /// </summary> + /// <value>The clients.</value> + public List<string> Clients { get; set; } + + public LiveTvTunerInfoDto() + { + Clients = new List<string>(); } } diff --git a/MediaBrowser.Providers/Movies/MovieDbProvider.cs b/MediaBrowser.Providers/Movies/MovieDbProvider.cs index d4367a5db..d6937293c 100644 --- a/MediaBrowser.Providers/Movies/MovieDbProvider.cs +++ b/MediaBrowser.Providers/Movies/MovieDbProvider.cs @@ -796,9 +796,6 @@ namespace MediaBrowser.Providers.Movies boxset.OfficialRating = firstChild != null ? firstChild.OfficialRating : null; } - if (movieData.runtime > 0) - movie.OriginalRunTimeTicks = TimeSpan.FromMinutes(movieData.runtime).Ticks; - //studios if (movieData.production_companies != null && !movie.LockedFields.Contains(MetadataFields.Studios)) { diff --git a/MediaBrowser.Providers/Savers/EpisodeXmlSaver.cs b/MediaBrowser.Providers/Savers/EpisodeXmlSaver.cs index 4e2139bb6..b7cfe8aee 100644 --- a/MediaBrowser.Providers/Savers/EpisodeXmlSaver.cs +++ b/MediaBrowser.Providers/Savers/EpisodeXmlSaver.cs @@ -109,7 +109,7 @@ namespace MediaBrowser.Providers.Savers if (episode.PremiereDate.HasValue) { - builder.Append("<FirstAired>" + SecurityElement.Escape(episode.PremiereDate.Value.ToString("yyyy-MM-dd")) + "</FirstAired>"); + builder.Append("<FirstAired>" + SecurityElement.Escape(episode.PremiereDate.Value.ToLocalTime().ToString("yyyy-MM-dd")) + "</FirstAired>"); } XmlSaverHelpers.AddCommonNodes(item, builder); diff --git a/MediaBrowser.Providers/Savers/SeriesXmlSaver.cs b/MediaBrowser.Providers/Savers/SeriesXmlSaver.cs index 131d568a1..29ca12f7f 100644 --- a/MediaBrowser.Providers/Savers/SeriesXmlSaver.cs +++ b/MediaBrowser.Providers/Savers/SeriesXmlSaver.cs @@ -95,7 +95,7 @@ namespace MediaBrowser.Providers.Savers if (series.PremiereDate.HasValue) { - builder.Append("<FirstAired>" + SecurityElement.Escape(series.PremiereDate.Value.ToString("yyyy-MM-dd")) + "</FirstAired>"); + builder.Append("<FirstAired>" + SecurityElement.Escape(series.PremiereDate.Value.ToLocalTime().ToString("yyyy-MM-dd")) + "</FirstAired>"); } XmlSaverHelpers.AddCommonNodes(item, builder); diff --git a/MediaBrowser.Providers/Savers/XmlSaverHelpers.cs b/MediaBrowser.Providers/Savers/XmlSaverHelpers.cs index 0a03f98f5..230dab717 100644 --- a/MediaBrowser.Providers/Savers/XmlSaverHelpers.cs +++ b/MediaBrowser.Providers/Savers/XmlSaverHelpers.cs @@ -252,11 +252,11 @@ namespace MediaBrowser.Providers.Savers { if (item is Person) { - builder.Append("<BirthDate>" + SecurityElement.Escape(item.PremiereDate.Value.ToString("yyyy-MM-dd")) + "</BirthDate>"); + builder.Append("<BirthDate>" + SecurityElement.Escape(item.PremiereDate.Value.ToLocalTime().ToString("yyyy-MM-dd")) + "</BirthDate>"); } else if (!(item is Episode)) { - builder.Append("<PremiereDate>" + SecurityElement.Escape(item.PremiereDate.Value.ToString("yyyy-MM-dd")) + "</PremiereDate>"); + builder.Append("<PremiereDate>" + SecurityElement.Escape(item.PremiereDate.Value.ToLocalTime().ToString("yyyy-MM-dd")) + "</PremiereDate>"); } } @@ -358,7 +358,7 @@ namespace MediaBrowser.Providers.Savers } // Use original runtime here, actual file runtime later in MediaInfo - var runTimeTicks = item.OriginalRunTimeTicks ?? item.RunTimeTicks; + var runTimeTicks = item.RunTimeTicks; if (runTimeTicks.HasValue) { diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index 7fa299050..7a9735e0e 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -649,11 +649,6 @@ namespace MediaBrowser.Server.Implementations.Dto dto.DateCreated = item.DateCreated; } - if (fields.Contains(ItemFields.OriginalRunTimeTicks)) - { - dto.OriginalRunTimeTicks = item.OriginalRunTimeTicks; - } - dto.DisplayMediaType = item.DisplayMediaType; if (fields.Contains(ItemFields.Settings)) diff --git a/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs b/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs index f9f54199f..ece21df7a 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs +++ b/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs @@ -1,20 +1,21 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using MediaBrowser.Common.IO; +using MediaBrowser.Common.IO; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.FileOrganization; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.FileOrganization; using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.FileOrganization @@ -28,7 +29,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization private readonly IFileOrganizationService _organizationService; private readonly IServerConfigurationManager _config; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); public EpisodeFileOrganizer(IFileOrganizationService organizationService, IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger, ILibraryManager libraryManager, IDirectoryWatchers directoryWatchers) { @@ -154,28 +155,79 @@ namespace MediaBrowser.Server.Implementations.FileOrganization _logger.Info("Sorting file {0} to new path {1}", sourcePath, newPath); result.TargetPath = newPath; - var existing = GetDuplicatePaths(result.TargetPath, series, seasonNumber, episodeNumber); + var fileExists = File.Exists(result.TargetPath); + var otherDuplicatePaths = GetOtherDuplicatePaths(result.TargetPath, series, seasonNumber, episodeNumber, endingEpiosdeNumber); - if (!overwriteExisting && existing.Count > 0) + if (!overwriteExisting && (fileExists || otherDuplicatePaths.Count > 0)) { result.Status = FileSortingStatus.SkippedExisting; result.StatusMessage = string.Empty; + result.DuplicatePaths = otherDuplicatePaths; return; } PerformFileSorting(options, result); + + if (overwriteExisting) + { + foreach (var path in otherDuplicatePaths) + { + _logger.Debug("Removing duplicate episode {0}", path); + + try + { + File.Delete(path); + } + catch (IOException ex) + { + _logger.ErrorException("Error removing duplicate episode", ex, path); + } + } + } } - private List<string> GetDuplicatePaths(string targetPath, Series series, int seasonNumber, int episodeNumber) + private List<string> GetOtherDuplicatePaths(string targetPath, Series series, int seasonNumber, int episodeNumber, int? endingEpisodeNumber) { - var list = new List<string>(); + var episodePaths = series.RecursiveChildren + .OfType<Episode>() + .Where(i => + { + var locationType = i.LocationType; + + // Must be file system based and match exactly + if (locationType != LocationType.Remote && + locationType != LocationType.Virtual && + i.ParentIndexNumber.HasValue && + i.ParentIndexNumber.Value == seasonNumber && + i.IndexNumber.HasValue && + i.IndexNumber.Value == episodeNumber) + { - if (File.Exists(targetPath)) - { - list.Add(targetPath); - } + if (endingEpisodeNumber.HasValue || i.IndexNumberEnd.HasValue) + { + return endingEpisodeNumber.HasValue && i.IndexNumberEnd.HasValue && + endingEpisodeNumber.Value == i.IndexNumberEnd.Value; + } + + return true; + } - return list; + return false; + }) + .Select(i => i.Path) + .ToList(); + + var folder = Path.GetDirectoryName(targetPath); + var targetFileNameWithoutExtension = Path.GetFileNameWithoutExtension(targetPath); + + var filesOfOtherExtensions = Directory.EnumerateFiles(folder, "*", SearchOption.TopDirectoryOnly) + .Where(i => EntityResolutionHelper.IsVideoFile(i) && string.Equals(Path.GetFileNameWithoutExtension(i), targetFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)); + + episodePaths.AddRange(filesOfOtherExtensions); + + return episodePaths.Where(i => !string.Equals(i, targetPath, StringComparison.OrdinalIgnoreCase)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); } private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizationResult result) @@ -185,7 +237,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization Directory.CreateDirectory(Path.GetDirectoryName(result.TargetPath)); var copy = File.Exists(result.TargetPath); - + try { if (copy) diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index d8af92991..5b9e73996 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -35,7 +35,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV } // If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something - if (season != null || parent.Parents.OfType<Series>().Any()) + if (season != null || parent is Series || parent.Parents.OfType<Series>().Any()) { Episode episode = null; diff --git a/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs index d04ebe32d..f1e10e175 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs @@ -76,22 +76,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv private async Task<bool> DownloadImage(LiveTvChannel item, CancellationToken cancellationToken) { - var channelInfo = item.ChannelInfo; - Stream imageStream = null; string contentType = null; - if (!string.IsNullOrEmpty(channelInfo.ImagePath)) + if (!string.IsNullOrEmpty(item.ProviderImagePath)) { - contentType = "image/" + Path.GetExtension(channelInfo.ImagePath).ToLower(); - imageStream = _fileSystem.GetFileStream(channelInfo.ImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, true); + contentType = "image/" + Path.GetExtension(item.ProviderImagePath).ToLower(); + imageStream = _fileSystem.GetFileStream(item.ProviderImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, true); } - else if (!string.IsNullOrEmpty(channelInfo.ImageUrl)) + else if (!string.IsNullOrEmpty(item.ProviderImageUrl)) { var options = new HttpRequestOptions { CancellationToken = cancellationToken, - Url = channelInfo.ImageUrl + Url = item.ProviderImageUrl }; var response = await _httpClient.GetResponse(options).ConfigureAwait(false); @@ -105,7 +103,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv imageStream = response.Content; contentType = response.ContentType; } - else if (channelInfo.HasImage ?? true) + else if (item.HasProviderImage ?? true) { var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, item.ServiceName, StringComparison.OrdinalIgnoreCase)); @@ -113,7 +111,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv { try { - var response = await service.GetChannelImageAsync(channelInfo.Id, cancellationToken).ConfigureAwait(false); + var response = await service.GetChannelImageAsync(item.ExternalId, cancellationToken).ConfigureAwait(false); if (response != null) { @@ -131,7 +129,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv if (imageStream != null) { // Dummy up the original url - var url = item.ServiceName + channelInfo.Id; + var url = item.ServiceName + item.ExternalId; await _providerManager.SaveImage(item, imageStream, contentType, ImageType.Primary, null, url, cancellationToken).ConfigureAwait(false); return true; diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs index 2fea919b0..4805adb1f 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -74,7 +74,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv if (channel != null) { - dto.ChannelName = channel.ChannelInfo.Name; + dto.ChannelName = channel.Name; } return dto; @@ -224,7 +224,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv MediaStreams = _itemRepo.GetMediaStreams(new MediaStreamQuery { - ItemId = recording.Id + ItemId = recording.Id }).ToList() }; @@ -271,6 +271,32 @@ namespace MediaBrowser.Server.Implementations.LiveTv return dto; } + public LiveTvTunerInfoDto GetTunerInfoDto(string serviceName, LiveTvTunerInfo info, string channelName) + { + var dto = new LiveTvTunerInfoDto + { + Name = info.Name, + Id = info.Id, + Clients = info.Clients, + ProgramName = info.ProgramName, + SourceType = info.SourceType, + Status = info.Status, + ChannelName = channelName + }; + + if (!string.IsNullOrEmpty(info.ChannelId)) + { + dto.ChannelId = GetInternalChannelId(serviceName, info.ChannelId).ToString("N"); + } + + if (!string.IsNullOrEmpty(info.RecordingId)) + { + dto.RecordingId = GetInternalRecordingId(serviceName, info.RecordingId).ToString("N"); + } + + return dto; + } + /// <summary> /// Gets the channel info dto. /// </summary> @@ -280,18 +306,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv /// <returns>ChannelInfoDto.</returns> public ChannelInfoDto GetChannelInfoDto(LiveTvChannel info, LiveTvProgram currentProgram, User user = null) { - var channelInfo = info.ChannelInfo; - var dto = new ChannelInfoDto { Name = info.Name, ServiceName = info.ServiceName, - ChannelType = channelInfo.ChannelType, - Number = channelInfo.Number, + ChannelType = info.ChannelType, + Number = info.Number, Type = info.GetClientTypeName(), Id = info.Id.ToString("N"), MediaType = info.MediaType, - ExternalId = channelInfo.Id + ExternalId = info.ExternalId }; if (user != null) @@ -316,37 +340,40 @@ namespace MediaBrowser.Server.Implementations.LiveTv public ProgramInfoDto GetProgramInfoDto(LiveTvProgram item, LiveTvChannel channel, User user = null) { - var program = item.ProgramInfo; - var dto = new ProgramInfoDto { - Id = GetInternalProgramId(item.ServiceName, program.Id).ToString("N"), - ChannelId = GetInternalChannelId(item.ServiceName, program.ChannelId).ToString("N"), - Overview = program.Overview, - EndDate = program.EndDate, - Genres = program.Genres, - ExternalId = program.Id, - Name = program.Name, + Id = GetInternalProgramId(item.ServiceName, item.ExternalId).ToString("N"), + ChannelId = GetInternalChannelId(item.ServiceName, item.ExternalChannelId).ToString("N"), + Overview = item.Overview, + Genres = item.Genres, + ExternalId = item.ExternalId, + Name = item.Name, ServiceName = item.ServiceName, - StartDate = program.StartDate, - OfficialRating = program.OfficialRating, - IsHD = program.IsHD, - OriginalAirDate = program.OriginalAirDate, - Audio = program.Audio, - CommunityRating = GetClientCommunityRating(program.CommunityRating), - IsRepeat = program.IsRepeat, - EpisodeTitle = program.EpisodeTitle, - IsMovie = program.IsMovie, - IsSeries = program.IsSeries, - IsSports = program.IsSports, - IsLive = program.IsLive, - IsNews = program.IsNews, - IsKids = program.IsKids, - IsPremiere = program.IsPremiere, - RunTimeTicks = (program.EndDate - program.StartDate).Ticks, + StartDate = item.StartDate, + OfficialRating = item.OfficialRating, + IsHD = item.IsHD, + OriginalAirDate = item.PremiereDate, + Audio = item.Audio, + CommunityRating = GetClientCommunityRating(item.CommunityRating), + IsRepeat = item.IsRepeat, + EpisodeTitle = item.EpisodeTitle, + IsMovie = item.IsMovie, + IsSeries = item.IsSeries, + IsSports = item.IsSports, + IsLive = item.IsLive, + IsNews = item.IsNews, + IsKids = item.IsKids, + IsPremiere = item.IsPremiere, Type = "Program" }; + if (item.EndDate.HasValue) + { + dto.EndDate = item.EndDate.Value; + + dto.RunTimeTicks = (item.EndDate.Value - item.StartDate).Ticks; + } + if (channel != null) { dto.ChannelName = channel.Name; diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs index f62efd9da..e256d7da5 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs @@ -1,7 +1,6 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Common.ScheduledTasks; -using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; @@ -126,9 +125,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv { double number = 0; - if (!string.IsNullOrEmpty(i.ChannelInfo.Number)) + if (!string.IsNullOrEmpty(i.Number)) { - double.TryParse(i.ChannelInfo.Number, out number); + double.TryParse(i.Number, out number); } return number; @@ -140,9 +139,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv { double number = 0; - if (!string.IsNullOrEmpty(i.ChannelInfo.Number)) + if (!string.IsNullOrEmpty(i.Number)) { - double.TryParse(i.ChannelInfo.Number, out number); + double.TryParse(i.Number, out number); } return number; @@ -163,7 +162,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv } var returnChannels = allEnumerable - .Select(i => _tvDtoService.GetChannelInfoDto(i, GetCurrentProgram(i.ChannelInfo.Id), user)) + .Select(i => _tvDtoService.GetChannelInfoDto(i, GetCurrentProgram(i.ExternalId), user)) .ToArray(); var result = new QueryResult<ChannelInfoDto> @@ -251,9 +250,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv var channel = GetInternalChannel(id); - _logger.Info("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ChannelInfo.Id); + _logger.Info("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId); - var result = await service.GetChannelStream(channel.ChannelInfo.Id, cancellationToken).ConfigureAwait(false); + var result = await service.GetChannelStream(channel.ExternalId, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(result.Id)) { @@ -313,8 +312,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv isNew = true; } - item.ChannelInfo = channelInfo; + item.ChannelType = channelInfo.ChannelType; + item.ProviderImageUrl = channelInfo.ImageUrl; + item.HasProviderImage = channelInfo.HasImage; + item.ProviderImagePath = channelInfo.ImagePath; + item.ExternalId = channelInfo.Id; item.ServiceName = serviceName; + item.Number = channelInfo.Number; + + if (string.IsNullOrEmpty(item.Name)) + { + item.Name = channelInfo.Name; + } // Set this now so we don't cause additional file system access during provider executions item.ResetResolveArgs(fileInfo); @@ -346,9 +355,34 @@ namespace MediaBrowser.Server.Implementations.LiveTv } item.ChannelType = channelType; - item.ProgramInfo = info; item.ServiceName = serviceName; + item.Audio = info.Audio; + item.ExternalChannelId = info.ChannelId; + item.CommunityRating = info.CommunityRating; + item.EndDate = info.EndDate; + item.EpisodeTitle = info.EpisodeTitle; + item.ExternalId = info.Id; + item.Genres = info.Genres; + item.HasProviderImage = info.HasImage; + item.IsHD = info.IsHD; + item.IsKids = info.IsKids; + item.IsLive = info.IsLive; + item.IsMovie = info.IsMovie; + item.IsNews = info.IsNews; + item.IsPremiere = info.IsPremiere; + item.IsRepeat = info.IsRepeat; + item.IsSeries = info.IsSeries; + item.IsSports = info.IsSports; + item.Name = info.Name; + item.OfficialRating = info.OfficialRating; + item.Overview = info.Overview; + item.PremiereDate = info.OriginalAirDate; + item.ProviderImagePath = info.ImagePath; + item.ProviderImageUrl = info.ImageUrl; + item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks; + item.StartDate = info.StartDate; + await item.RefreshMetadata(cancellationToken, forceSave: isNew, resetResolveArgs: false); return item; @@ -410,7 +444,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv private LiveTvChannel GetChannel(LiveTvProgram program) { - var programChannelId = program.ProgramInfo.ChannelId; + var programChannelId = program.ExternalChannelId; var internalProgramChannelId = _tvDtoService.GetInternalChannelId(program.ServiceName, programChannelId); @@ -438,28 +472,28 @@ namespace MediaBrowser.Server.Implementations.LiveTv { var val = query.MinEndDate.Value; - programs = programs.Where(i => i.ProgramInfo.EndDate >= val); + programs = programs.Where(i => i.EndDate.HasValue && i.EndDate.Value >= val); } if (query.MinStartDate.HasValue) { var val = query.MinStartDate.Value; - programs = programs.Where(i => i.ProgramInfo.StartDate >= val); + programs = programs.Where(i => i.StartDate >= val); } if (query.MaxEndDate.HasValue) { var val = query.MaxEndDate.Value; - programs = programs.Where(i => i.ProgramInfo.EndDate <= val); + programs = programs.Where(i => i.EndDate.HasValue && i.EndDate.Value <= val); } if (query.MaxStartDate.HasValue) { var val = query.MaxStartDate.Value; - programs = programs.Where(i => i.ProgramInfo.StartDate <= val); + programs = programs.Where(i => i.StartDate <= val); } if (query.ChannelIdList.Length > 0) @@ -469,7 +503,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv programs = programs.Where(i => { - var programChannelId = i.ProgramInfo.ChannelId; + var programChannelId = i.ExternalChannelId; var internalProgramChannelId = _tvDtoService.GetInternalChannelId(serviceName, programChannelId); @@ -537,13 +571,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv .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); + programs = programList.OrderByDescending(i => GetRecommendationScore(i, user.Id, serviceName, genres)) + .ThenBy(i => i.StartDate); if (query.Limit.HasValue) { programs = programs.Take(query.Limit.Value) - .OrderBy(i => i.ProgramInfo.StartDate); + .OrderBy(i => i.StartDate); } var returnArray = programs @@ -566,7 +600,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv return result; } - private int GetRecommendationScore(ProgramInfo program, Guid userId, string serviceName, Dictionary<string, Genre> genres) + private int GetRecommendationScore(LiveTvProgram program, Guid userId, string serviceName, Dictionary<string, Genre> genres) { var score = 0; @@ -580,7 +614,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv score++; } - var internalChannelId = _tvDtoService.GetInternalChannelId(serviceName, program.ChannelId); + var internalChannelId = _tvDtoService.GetInternalChannelId(serviceName, program.ExternalChannelId); var channel = GetInternalChannel(internalChannelId); var channelUserdata = _userDataManager.GetUserData(userId, channel.GetUserDataKey()); @@ -724,9 +758,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv 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 channelPrograms = await service.GetProgramsAsync(currentChannel.ExternalId, start, end, cancellationToken).ConfigureAwait(false); - var programTasks = channelPrograms.Select(program => GetProgram(program, currentChannel.ChannelInfo.ChannelType, service.Name, cancellationToken)); + var programTasks = channelPrograms.Select(program => GetProgram(program, currentChannel.ChannelType, service.Name, cancellationToken)); var programEntities = await Task.WhenAll(programTasks).ConfigureAwait(false); programs.AddRange(programEntities); @@ -1033,7 +1067,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv { var internalChannelId = _tvDtoService.GetInternalChannelId(service.Name, i.ChannelId); var channel = GetInternalChannel(internalChannelId); - channelName = channel == null ? null : channel.ChannelInfo.Name; + channelName = channel == null ? null : channel.Name; } return _tvDtoService.GetSeriesTimerInfoDto(i, service, channelName); @@ -1052,7 +1086,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv { var channel = GetInternalChannel(id); - var dto = _tvDtoService.GetChannelInfoDto(channel, GetCurrentProgram(channel.ChannelInfo.Id), user); + var dto = _tvDtoService.GetChannelInfoDto(channel, GetCurrentProgram(channel.ExternalId), user); return Task.FromResult(dto); } @@ -1062,15 +1096,48 @@ namespace MediaBrowser.Server.Implementations.LiveTv var now = DateTime.UtcNow; return _programs.Values - .Where(i => string.Equals(externalChannelId, i.ProgramInfo.ChannelId, StringComparison.OrdinalIgnoreCase)) - .OrderBy(i => i.ProgramInfo.StartDate) - .SkipWhile(i => now >= i.ProgramInfo.EndDate) + .Where(i => string.Equals(externalChannelId, i.ExternalChannelId, StringComparison.OrdinalIgnoreCase)) + .OrderBy(i => i.StartDate) + .SkipWhile(i => now >= (i.EndDate ?? DateTime.MinValue)) .FirstOrDefault(); } - private async Task<SeriesTimerInfo> GetNewTimerDefaultsInternal(CancellationToken cancellationToken, ProgramInfo program = null) + private async Task<SeriesTimerInfo> GetNewTimerDefaultsInternal(CancellationToken cancellationToken, LiveTvProgram program = null) { - var info = await ActiveService.GetNewTimerDefaultsAsync(cancellationToken, program).ConfigureAwait(false); + ProgramInfo programInfo = null; + + if (program != null) + { + programInfo = new ProgramInfo + { + Audio = program.Audio, + ChannelId = program.ExternalChannelId, + CommunityRating = program.CommunityRating, + EndDate = program.EndDate ?? DateTime.MinValue, + EpisodeTitle = program.EpisodeTitle, + Genres = program.Genres, + HasImage = program.HasProviderImage, + Id = program.ExternalId, + IsHD = program.IsHD, + IsKids = program.IsKids, + IsLive = program.IsLive, + IsMovie = program.IsMovie, + IsNews = program.IsNews, + IsPremiere = program.IsPremiere, + IsRepeat = program.IsRepeat, + IsSeries = program.IsSeries, + IsSports = program.IsSports, + OriginalAirDate = program.PremiereDate, + Overview = program.Overview, + StartDate = program.StartDate, + ImagePath = program.ProviderImagePath, + ImageUrl = program.ProviderImageUrl, + Name = program.Name, + OfficialRating = program.OfficialRating + }; + } + + var info = await ActiveService.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false); info.Id = null; @@ -1088,7 +1155,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv public async Task<SeriesTimerInfoDto> GetNewTimerDefaults(string programId, CancellationToken cancellationToken) { - var program = GetInternalProgram(programId).ProgramInfo; + var program = GetInternalProgram(programId); var programDto = await GetProgram(programId, cancellationToken).ConfigureAwait(false); var defaults = await GetNewTimerDefaultsInternal(cancellationToken, program).ConfigureAwait(false); @@ -1104,13 +1171,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv info.Name = program.Name; info.ChannelId = programDto.ChannelId; info.ChannelName = programDto.ChannelName; - info.EndDate = program.EndDate; info.StartDate = program.StartDate; info.Name = program.Name; info.Overview = program.Overview; info.ProgramId = programDto.Id; info.ExternalProgramId = programDto.ExternalId; + if (program.EndDate.HasValue) + { + info.EndDate = program.EndDate.Value; + } + return info; } @@ -1299,8 +1370,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv { var programs = _programs.ToList(); - var startDate = programs.Select(i => i.Value.ProgramInfo.StartDate).Min(); - var endDate = programs.Select(i => i.Value.ProgramInfo.StartDate).Max(); + var startDate = programs.Select(i => i.Value.StartDate).Min(); + var endDate = programs.Select(i => i.Value.StartDate).Max(); return new GuideInfo { @@ -1340,7 +1411,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv } } - public async Task<IEnumerable<LiveTvServiceInfo>> GetServiceInfos(CancellationToken cancellationToken) + private async Task<IEnumerable<LiveTvServiceInfo>> GetServiceInfos(CancellationToken cancellationToken) { var tasks = Services.Select(i => GetServiceInfo(i, cancellationToken)); @@ -1363,6 +1434,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv info.Version = statusInfo.Version; info.HasUpdateAvailable = statusInfo.HasUpdateAvailable; info.HomePageUrl = service.HomePageUrl; + + info.Tuners = statusInfo.Tuners.Select(i => + { + string channelName = null; + + if (!string.IsNullOrEmpty(i.ChannelId)) + { + var internalChannelId = _tvDtoService.GetInternalChannelId(service.Name, i.ChannelId); + var channel = GetInternalChannel(internalChannelId); + channelName = channel == null ? null : channel.Name; + } + + return _tvDtoService.GetTunerInfoDto(service.Name, i, channelName); + + }).ToList(); } catch (Exception ex) { @@ -1374,5 +1460,41 @@ namespace MediaBrowser.Server.Implementations.LiveTv return info; } + + public async Task<LiveTvInfo> GetLiveTvInfo(CancellationToken cancellationToken) + { + var services = await GetServiceInfos(CancellationToken.None).ConfigureAwait(false); + var servicesList = services.ToList(); + + var activeServiceInfo = ActiveService == null ? null : + servicesList.FirstOrDefault(i => string.Equals(i.Name, ActiveService.Name, StringComparison.OrdinalIgnoreCase)); + + var info = new LiveTvInfo + { + Services = servicesList.ToList(), + ActiveServiceName = activeServiceInfo == null ? null : activeServiceInfo.Name, + IsEnabled = ActiveService != null, + Status = activeServiceInfo == null ? LiveTvServiceStatus.Unavailable : activeServiceInfo.Status, + StatusMessage = activeServiceInfo == null ? null : activeServiceInfo.StatusMessage + }; + + info.EnabledUsers = _userManager.Users + .Where(i => i.Configuration.EnableLiveTvAccess && info.IsEnabled) + .Select(i => i.Id.ToString("N")) + .ToList(); + + return info; + } + + /// <summary> + /// Resets the tuner. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public Task ResetTuner(string id, CancellationToken cancellationToken) + { + return ActiveService.ResetTuner(id, cancellationToken); + } } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs index 7c343f77c..041925cdd 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs @@ -76,22 +76,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv private async Task<bool> DownloadImage(LiveTvProgram item, CancellationToken cancellationToken) { - var programInfo = item.ProgramInfo; - Stream imageStream = null; string contentType = null; - if (!string.IsNullOrEmpty(programInfo.ImagePath)) + if (!string.IsNullOrEmpty(item.ProviderImagePath)) { - contentType = "image/" + Path.GetExtension(programInfo.ImagePath).ToLower(); - imageStream = _fileSystem.GetFileStream(programInfo.ImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, true); + contentType = "image/" + Path.GetExtension(item.ProviderImagePath).ToLower(); + imageStream = _fileSystem.GetFileStream(item.ProviderImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, true); } - else if (!string.IsNullOrEmpty(programInfo.ImageUrl)) + else if (!string.IsNullOrEmpty(item.ProviderImageUrl)) { var options = new HttpRequestOptions { CancellationToken = cancellationToken, - Url = programInfo.ImageUrl + Url = item.ProviderImageUrl }; var response = await _httpClient.GetResponse(options).ConfigureAwait(false); @@ -105,7 +103,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv imageStream = response.Content; contentType = response.ContentType; } - else if (programInfo.HasImage ?? true) + else if (item.HasProviderImage ?? true) { var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, item.ServiceName, StringComparison.OrdinalIgnoreCase)); @@ -113,7 +111,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv { try { - var response = await service.GetProgramImageAsync(programInfo.Id, programInfo.ChannelId, cancellationToken).ConfigureAwait(false); + var response = await service.GetProgramImageAsync(item.ExternalId, item.ExternalChannelId, cancellationToken).ConfigureAwait(false); if (response != null) { @@ -131,7 +129,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv if (imageStream != null) { // Dummy up the original url - var url = item.ServiceName + programInfo.Id; + var url = item.ServiceName + item.ExternalId; await _providerManager.SaveImage(item, imageStream, contentType, ImageType.Primary, null, url, cancellationToken).ConfigureAwait(false); return true; diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteFileOrganizationRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteFileOrganizationRepository.cs index 4463ac6f3..86bf0d71c 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteFileOrganizationRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteFileOrganizationRepository.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Data; using System.Globalization; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -48,8 +49,8 @@ namespace MediaBrowser.Server.Implementations.Persistence string[] queries = { - "create table if not exists organizationresults (ResultId GUID PRIMARY KEY, OriginalPath TEXT, TargetPath TEXT, OrganizationDate datetime, Status TEXT, OrganizationType TEXT, StatusMessage TEXT, ExtractedName TEXT, ExtractedYear int null, ExtractedSeasonNumber int null, ExtractedEpisodeNumber int null, ExtractedEndingEpisodeNumber int null)", - "create index if not exists idx_organizationresults on organizationresults(ResultId)", + "create table if not exists OrganizerResults (ResultId GUID PRIMARY KEY, OriginalPath TEXT, TargetPath TEXT, OrganizationDate datetime, Status TEXT, OrganizationType TEXT, StatusMessage TEXT, ExtractedName TEXT, ExtractedYear int null, ExtractedSeasonNumber int null, ExtractedEpisodeNumber int null, ExtractedEndingEpisodeNumber, DuplicatePaths TEXT int null)", + "create index if not exists idx_OrganizerResults on OrganizerResults(ResultId)", //pragmas "pragma temp_store = memory", @@ -67,7 +68,7 @@ namespace MediaBrowser.Server.Implementations.Persistence private void PrepareStatements() { _saveResultCommand = _connection.CreateCommand(); - _saveResultCommand.CommandText = "replace into organizationresults (ResultId, OriginalPath, TargetPath, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber) values (@ResultId, @OriginalPath, @TargetPath, @OrganizationDate, @Status, @OrganizationType, @StatusMessage, @ExtractedName, @ExtractedYear, @ExtractedSeasonNumber, @ExtractedEpisodeNumber, @ExtractedEndingEpisodeNumber)"; + _saveResultCommand.CommandText = "replace into OrganizerResults (ResultId, OriginalPath, TargetPath, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths) values (@ResultId, @OriginalPath, @TargetPath, @OrganizationDate, @Status, @OrganizationType, @StatusMessage, @ExtractedName, @ExtractedYear, @ExtractedSeasonNumber, @ExtractedEpisodeNumber, @ExtractedEndingEpisodeNumber, @DuplicatePaths)"; _saveResultCommand.Parameters.Add(_saveResultCommand, "@ResultId"); _saveResultCommand.Parameters.Add(_saveResultCommand, "@OriginalPath"); @@ -81,14 +82,15 @@ namespace MediaBrowser.Server.Implementations.Persistence _saveResultCommand.Parameters.Add(_saveResultCommand, "@ExtractedSeasonNumber"); _saveResultCommand.Parameters.Add(_saveResultCommand, "@ExtractedEpisodeNumber"); _saveResultCommand.Parameters.Add(_saveResultCommand, "@ExtractedEndingEpisodeNumber"); + _saveResultCommand.Parameters.Add(_saveResultCommand, "@DuplicatePaths"); _deleteResultCommand = _connection.CreateCommand(); - _deleteResultCommand.CommandText = "delete from organizationresults where ResultId = @ResultId"; + _deleteResultCommand.CommandText = "delete from OrganizerResults where ResultId = @ResultId"; _deleteResultCommand.Parameters.Add(_saveResultCommand, "@ResultId"); _deleteAllCommand = _connection.CreateCommand(); - _deleteAllCommand.CommandText = "delete from organizationresults"; + _deleteAllCommand.CommandText = "delete from OrganizerResults"; } public async Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken) @@ -120,6 +122,7 @@ namespace MediaBrowser.Server.Implementations.Persistence _saveResultCommand.GetParameter(9).Value = result.ExtractedSeasonNumber; _saveResultCommand.GetParameter(10).Value = result.ExtractedEpisodeNumber; _saveResultCommand.GetParameter(11).Value = result.ExtractedEndingEpisodeNumber; + _saveResultCommand.GetParameter(12).Value = string.Join("|", result.DuplicatePaths.ToArray()); _saveResultCommand.Transaction = transaction; @@ -268,11 +271,11 @@ namespace MediaBrowser.Server.Implementations.Persistence using (var cmd = _connection.CreateCommand()) { - cmd.CommandText = "SELECT ResultId, OriginalPath, TargetPath, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber from organizationresults"; + cmd.CommandText = "SELECT ResultId, OriginalPath, TargetPath, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths from OrganizerResults"; if (query.StartIndex.HasValue && query.StartIndex.Value > 0) { - cmd.CommandText += string.Format(" WHERE ResultId NOT IN (SELECT ResultId FROM organizationresults ORDER BY OrganizationDate desc LIMIT {0})", + cmd.CommandText += string.Format(" WHERE ResultId NOT IN (SELECT ResultId FROM OrganizerResults ORDER BY OrganizationDate desc LIMIT {0})", query.StartIndex.Value.ToString(_usCulture)); } @@ -283,7 +286,7 @@ namespace MediaBrowser.Server.Implementations.Persistence cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); } - cmd.CommandText += "; select count (ResultId) from organizationresults"; + cmd.CommandText += "; select count (ResultId) from OrganizerResults"; var list = new List<FileOrganizationResult>(); var count = 0; @@ -320,7 +323,7 @@ namespace MediaBrowser.Server.Implementations.Persistence using (var cmd = _connection.CreateCommand()) { - cmd.CommandText = "select ResultId, OriginalPath, TargetPath, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber from organizationresults where ResultId=@Id"; + cmd.CommandText = "select ResultId, OriginalPath, TargetPath, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths from OrganizerResults where ResultId=@Id"; cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = guid; @@ -389,6 +392,11 @@ namespace MediaBrowser.Server.Implementations.Persistence result.ExtractedEndingEpisodeNumber = reader.GetInt32(11); } + if (!reader.IsDBNull(12)) + { + result.DuplicatePaths = reader.GetString(12).Split('|').Where(i => !string.IsNullOrEmpty(i)).ToList(); + } + return result; } diff --git a/MediaBrowser.WebDashboard/ApiClient.js b/MediaBrowser.WebDashboard/ApiClient.js index 7a90b12c9..375aacbce 100644 --- a/MediaBrowser.WebDashboard/ApiClient.js +++ b/MediaBrowser.WebDashboard/ApiClient.js @@ -655,6 +655,20 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi }); }; + self.resetLiveTvTuner = function (id) { + + if (!id) { + throw new Error("null id"); + } + + var url = self.getUrl("LiveTv/Tuners/" + id + "/Reset"); + + return self.ajax({ + type: "POST", + url: url + }); + }; + self.getLiveTvSeriesTimers = function (options) { var url = self.getUrl("LiveTv/SeriesTimers", options || {}); diff --git a/MediaBrowser.WebDashboard/packages.config b/MediaBrowser.WebDashboard/packages.config index d1427603d..4957f3563 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.243" targetFramework="net45" /> + <package id="MediaBrowser.ApiClient.Javascript" version="3.0.244" targetFramework="net45" /> </packages>
\ No newline at end of file diff --git a/Nuget/MediaBrowser.Common.Internal.nuspec b/Nuget/MediaBrowser.Common.Internal.nuspec index d239c4348..6729d8c72 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.307</version> + <version>3.0.309</version> <title>MediaBrowser.Common.Internal</title> <authors>Luke</authors> <owners>ebr,Luke,scottisafool</owners> @@ -12,9 +12,9 @@ <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.307" /> + <dependency id="MediaBrowser.Common" version="3.0.309" /> <dependency id="NLog" version="2.1.0" /> - <dependency id="SimpleInjector" version="2.4.0" /> + <dependency id="SimpleInjector" version="2.4.1" /> <dependency id="sharpcompress" version="0.10.2" /> </dependencies> </metadata> diff --git a/Nuget/MediaBrowser.Common.nuspec b/Nuget/MediaBrowser.Common.nuspec index 329ae9ec9..376cc722d 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.307</version> + <version>3.0.309</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 62323d1b3..22dd1aa9a 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.307</version> + <version>3.0.309</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.307" /> + <dependency id="MediaBrowser.Common" version="3.0.309" /> </dependencies> </metadata> <files> |
