aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs9
-rw-r--r--MediaBrowser.Api/BaseApiService.cs10
-rw-r--r--MediaBrowser.Api/Library/LibraryHelpers.cs70
-rw-r--r--MediaBrowser.Api/LiveTv/LiveTvService.cs61
-rw-r--r--MediaBrowser.Api/MediaBrowser.Api.csproj1
-rw-r--r--MediaBrowser.Api/Playback/BaseStreamingService.cs28
-rw-r--r--MediaBrowser.Api/Playback/Hls/AudioHlsService.cs2
-rw-r--r--MediaBrowser.Api/Playback/Hls/BaseHlsService.cs29
-rw-r--r--MediaBrowser.Api/Playback/Hls/HlsSegmentResponseFilter.cs53
-rw-r--r--MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs38
-rw-r--r--MediaBrowser.Api/Playback/Hls/VideoHlsService.cs217
-rw-r--r--MediaBrowser.Api/Playback/Progressive/AudioService.cs2
-rw-r--r--MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs2
-rw-r--r--MediaBrowser.Api/Playback/Progressive/VideoService.cs2
-rw-r--r--MediaBrowser.Api/Playback/StreamState.cs8
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj2
-rw-r--r--MediaBrowser.Controller/Entities/Audio/Audio.cs2
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvManager.cs12
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvService.cs30
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvProgram.cs21
-rw-r--r--MediaBrowser.Controller/LiveTv/RecordingInfo.cs6
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj3
-rw-r--r--MediaBrowser.Controller/MediaInfo/FFMpegManager.cs3
-rw-r--r--MediaBrowser.Controller/MediaInfo/IMediaEncoder.cs (renamed from MediaBrowser.Common/MediaInfo/IMediaEncoder.cs)15
-rw-r--r--MediaBrowser.Controller/MediaInfo/InternalMediaInfoResult.cs (renamed from MediaBrowser.Common/MediaInfo/MediaInfoResult.cs)8
-rw-r--r--MediaBrowser.Controller/MediaInfo/MediaEncoderHelpers.cs21
-rw-r--r--MediaBrowser.Controller/Sorting/SortExtensions.cs143
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs12
-rw-r--r--MediaBrowser.Model/Configuration/UserConfiguration.cs4
-rw-r--r--MediaBrowser.Model/Dto/BaseItemDto.cs4
-rw-r--r--MediaBrowser.Model/Dto/BaseItemPerson.cs2
-rw-r--r--MediaBrowser.Model/Dto/ChapterInfoDto.cs2
-rw-r--r--MediaBrowser.Model/Dto/StudioDto.cs2
-rw-r--r--MediaBrowser.Model/Dto/UserDto.cs2
-rw-r--r--MediaBrowser.Model/Entities/BaseItemInfo.cs2
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs25
-rw-r--r--MediaBrowser.Model/LiveTv/ProgramQuery.cs27
-rw-r--r--MediaBrowser.Model/LiveTv/RecordingInfoDto.cs6
-rw-r--r--MediaBrowser.Model/Session/SessionInfoDto.cs4
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs4
-rw-r--r--MediaBrowser.Providers/MediaInfo/BaseFFProbeProvider.cs9
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeAudioInfoProvider.cs5
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfoProvider.cs7
-rw-r--r--MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs3
-rw-r--r--MediaBrowser.Providers/Movies/OpenMovieDatabaseProvider.cs10
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs8
-rw-r--r--MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs2
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs5
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs262
-rw-r--r--MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs30
-rw-r--r--MediaBrowser.ServerApplication/ApplicationHost.cs7
-rw-r--r--MediaBrowser.WebDashboard/Api/DashboardService.cs3
-rw-r--r--MediaBrowser.WebDashboard/ApiClient.js24
-rw-r--r--MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj8
-rw-r--r--MediaBrowser.WebDashboard/packages.config2
-rw-r--r--Nuget/MediaBrowser.Common.Internal.nuspec4
-rw-r--r--Nuget/MediaBrowser.Common.nuspec2
-rw-r--r--Nuget/MediaBrowser.Server.Core.nuspec4
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>