mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-07-09 03:04:24 -04:00
commit
66ad1699e2
@ -37,11 +37,14 @@ namespace MediaBrowser.Api
|
||||
|
||||
private readonly ISessionManager _sessionManager;
|
||||
|
||||
public readonly SemaphoreSlim TranscodingStartLock = new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApiEntryPoint" /> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="appPaths">The application paths.</param>
|
||||
/// <param name="sessionManager">The session manager.</param>
|
||||
public ApiEntryPoint(ILogger logger, IServerApplicationPaths appPaths, ISessionManager sessionManager)
|
||||
{
|
||||
Logger = logger;
|
||||
@ -99,7 +102,7 @@ namespace MediaBrowser.Api
|
||||
{
|
||||
var jobCount = _activeTranscodingJobs.Count;
|
||||
|
||||
Parallel.ForEach(_activeTranscodingJobs.ToList(), j => KillTranscodingJob(j, true));
|
||||
Parallel.ForEach(_activeTranscodingJobs.ToList(), j => KillTranscodingJob(j, path => true));
|
||||
|
||||
// Try to allow for some time to kill the ffmpeg processes and delete the partial stream files
|
||||
if (jobCount > 0)
|
||||
@ -119,14 +122,12 @@ namespace MediaBrowser.Api
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="process">The process.</param>
|
||||
/// <param name="startTimeTicks">The start time ticks.</param>
|
||||
/// <param name="deviceId">The device id.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="cancellationTokenSource">The cancellation token source.</param>
|
||||
public void OnTranscodeBeginning(string path,
|
||||
TranscodingJobType type,
|
||||
Process process,
|
||||
long? startTimeTicks,
|
||||
string deviceId,
|
||||
StreamState state,
|
||||
CancellationTokenSource cancellationTokenSource)
|
||||
@ -139,7 +140,6 @@ namespace MediaBrowser.Api
|
||||
Path = path,
|
||||
Process = process,
|
||||
ActiveRequestCount = 1,
|
||||
StartTimeTicks = startTimeTicks,
|
||||
DeviceId = deviceId,
|
||||
CancellationTokenSource = cancellationTokenSource
|
||||
});
|
||||
@ -214,10 +214,15 @@ namespace MediaBrowser.Api
|
||||
/// <param name="type">The type.</param>
|
||||
/// <returns><c>true</c> if [has active transcoding job] [the specified path]; otherwise, <c>false</c>.</returns>
|
||||
public bool HasActiveTranscodingJob(string path, TranscodingJobType type)
|
||||
{
|
||||
return GetTranscodingJob(path, type) != null;
|
||||
}
|
||||
|
||||
public TranscodingJob GetTranscodingJob(string path, TranscodingJobType type)
|
||||
{
|
||||
lock (_activeTranscodingJobs)
|
||||
{
|
||||
return _activeTranscodingJobs.Any(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
|
||||
return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
@ -290,34 +295,70 @@ namespace MediaBrowser.Api
|
||||
{
|
||||
var job = (TranscodingJob)state;
|
||||
|
||||
KillTranscodingJob(job, true);
|
||||
KillTranscodingJob(job, path => true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kills the single transcoding job.
|
||||
/// </summary>
|
||||
/// <param name="deviceId">The device id.</param>
|
||||
/// <param name="deleteFiles">if set to <c>true</c> [delete files].</param>
|
||||
/// <param name="deleteFiles">The delete files.</param>
|
||||
/// <param name="acquireLock">if set to <c>true</c> [acquire lock].</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="ArgumentNullException">deviceId</exception>
|
||||
/// <exception cref="System.ArgumentNullException">sourcePath</exception>
|
||||
internal void KillTranscodingJobs(string deviceId, bool deleteFiles)
|
||||
internal Task KillTranscodingJobs(string deviceId, Func<string, bool> deleteFiles, bool acquireLock)
|
||||
{
|
||||
if (string.IsNullOrEmpty(deviceId))
|
||||
{
|
||||
throw new ArgumentNullException("deviceId");
|
||||
}
|
||||
|
||||
return KillTranscodingJobs(j => string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase), deleteFiles, acquireLock);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kills the transcoding jobs.
|
||||
/// </summary>
|
||||
/// <param name="killJob">The kill job.</param>
|
||||
/// <param name="deleteFiles">The delete files.</param>
|
||||
/// <param name="acquireLock">if set to <c>true</c> [acquire lock].</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">deviceId</exception>
|
||||
internal async Task KillTranscodingJobs(Func<TranscodingJob,bool> killJob, Func<string, bool> deleteFiles, bool acquireLock)
|
||||
{
|
||||
var jobs = new List<TranscodingJob>();
|
||||
|
||||
lock (_activeTranscodingJobs)
|
||||
{
|
||||
// This is really only needed for HLS.
|
||||
// Progressive streams can stop on their own reliably
|
||||
jobs.AddRange(_activeTranscodingJobs.Where(i => string.Equals(deviceId, i.DeviceId, StringComparison.OrdinalIgnoreCase)));
|
||||
jobs.AddRange(_activeTranscodingJobs.Where(killJob));
|
||||
}
|
||||
|
||||
foreach (var job in jobs)
|
||||
if (jobs.Count == 0)
|
||||
{
|
||||
KillTranscodingJob(job, deleteFiles);
|
||||
return;
|
||||
}
|
||||
|
||||
if (acquireLock)
|
||||
{
|
||||
await TranscodingStartLock.WaitAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
KillTranscodingJob(job, deleteFiles);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (acquireLock)
|
||||
{
|
||||
TranscodingStartLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -325,8 +366,8 @@ namespace MediaBrowser.Api
|
||||
/// Kills the transcoding job.
|
||||
/// </summary>
|
||||
/// <param name="job">The job.</param>
|
||||
/// <param name="deleteFiles">if set to <c>true</c> [delete files].</param>
|
||||
private void KillTranscodingJob(TranscodingJob job, bool deleteFiles)
|
||||
/// <param name="delete">The delete.</param>
|
||||
private void KillTranscodingJob(TranscodingJob job, Func<string, bool> delete)
|
||||
{
|
||||
lock (_activeTranscodingJobs)
|
||||
{
|
||||
@ -378,7 +419,7 @@ namespace MediaBrowser.Api
|
||||
}
|
||||
}
|
||||
|
||||
if (deleteFiles)
|
||||
if (delete(job.Path))
|
||||
{
|
||||
DeletePartialStreamFiles(job.Path, job.Type, 0, 1500);
|
||||
}
|
||||
@ -386,7 +427,7 @@ namespace MediaBrowser.Api
|
||||
|
||||
private async void DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
|
||||
{
|
||||
if (retryCount >= 8)
|
||||
if (retryCount >= 10)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -440,6 +481,8 @@ namespace MediaBrowser.Api
|
||||
.Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1)
|
||||
.ToList();
|
||||
|
||||
Exception e = null;
|
||||
|
||||
foreach (var file in filesToDelete)
|
||||
{
|
||||
try
|
||||
@ -449,9 +492,15 @@ namespace MediaBrowser.Api
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
e = ex;
|
||||
Logger.ErrorException("Error deleting HLS file {0}", ex, file);
|
||||
}
|
||||
}
|
||||
|
||||
if (e != null)
|
||||
{
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -486,12 +535,13 @@ namespace MediaBrowser.Api
|
||||
/// <value>The kill timer.</value>
|
||||
public Timer KillTimer { get; set; }
|
||||
|
||||
public long? StartTimeTicks { get; set; }
|
||||
public string DeviceId { get; set; }
|
||||
|
||||
public CancellationTokenSource CancellationTokenSource { get; set; }
|
||||
|
||||
public object ProcessLock = new object();
|
||||
|
||||
public bool HasExited { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,4 +1,5 @@
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Themes;
|
||||
using MediaBrowser.Model.Themes;
|
||||
using ServiceStack;
|
||||
@ -47,6 +48,7 @@ namespace MediaBrowser.Api
|
||||
{
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class AppThemeService : BaseApiService
|
||||
{
|
||||
private readonly IAppThemeManager _themeManager;
|
||||
@ -92,7 +94,7 @@ namespace MediaBrowser.Api
|
||||
|
||||
var contentType = MimeTypes.GetMimeType(info.Path);
|
||||
|
||||
return ToCachedResult(cacheGuid, info.DateModified, cacheDuration, () => _fileSystem.GetFileStream(info.Path, FileMode.Open, FileAccess.Read, FileShare.Read), contentType);
|
||||
return ResultFactory.GetCachedResult(Request, cacheGuid, null, cacheDuration, () => _fileSystem.GetFileStream(info.Path, FileMode.Open, FileAccess.Read, FileShare.Read), contentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,190 +0,0 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using ServiceStack.Web;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Api
|
||||
{
|
||||
public class AuthorizationRequestFilterAttribute : Attribute, IHasRequestFilter
|
||||
{
|
||||
//This property will be resolved by the IoC container
|
||||
/// <summary>
|
||||
/// Gets or sets the user manager.
|
||||
/// </summary>
|
||||
/// <value>The user manager.</value>
|
||||
public IUserManager UserManager { get; set; }
|
||||
|
||||
public ISessionManager SessionManager { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the logger.
|
||||
/// </summary>
|
||||
/// <value>The logger.</value>
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The request filter is executed before the service.
|
||||
/// </summary>
|
||||
/// <param name="request">The http request wrapper</param>
|
||||
/// <param name="response">The http response wrapper</param>
|
||||
/// <param name="requestDto">The request DTO</param>
|
||||
public void RequestFilter(IRequest request, IResponse response, object requestDto)
|
||||
{
|
||||
//This code is executed before the service
|
||||
var auth = GetAuthorizationDictionary(request);
|
||||
|
||||
if (auth != null)
|
||||
{
|
||||
User user = null;
|
||||
|
||||
if (auth.ContainsKey("UserId"))
|
||||
{
|
||||
var userId = auth["UserId"];
|
||||
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
user = UserManager.GetUserById(new Guid(userId));
|
||||
}
|
||||
}
|
||||
|
||||
string deviceId;
|
||||
string device;
|
||||
string client;
|
||||
string version;
|
||||
|
||||
auth.TryGetValue("DeviceId", out deviceId);
|
||||
auth.TryGetValue("Device", out device);
|
||||
auth.TryGetValue("Client", out client);
|
||||
auth.TryGetValue("Version", out version);
|
||||
|
||||
if (!string.IsNullOrEmpty(client) && !string.IsNullOrEmpty(deviceId) && !string.IsNullOrEmpty(device) && !string.IsNullOrEmpty(version))
|
||||
{
|
||||
var remoteEndPoint = request.RemoteIp;
|
||||
|
||||
SessionManager.LogSessionActivity(client, version, deviceId, device, remoteEndPoint, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the auth.
|
||||
/// </summary>
|
||||
/// <param name="httpReq">The HTTP req.</param>
|
||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||
private static Dictionary<string, string> GetAuthorizationDictionary(IRequest httpReq)
|
||||
{
|
||||
var auth = httpReq.Headers["Authorization"];
|
||||
|
||||
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>
|
||||
/// <param name="httpReq">The HTTP req.</param>
|
||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||
public static AuthorizationInfo GetAuthorization(IRequest httpReq)
|
||||
{
|
||||
var auth = GetAuthorizationDictionary(httpReq);
|
||||
|
||||
string userId = null;
|
||||
string deviceId = null;
|
||||
string device = null;
|
||||
string client = null;
|
||||
string version = null;
|
||||
|
||||
if (auth != null)
|
||||
{
|
||||
auth.TryGetValue("UserId", out userId);
|
||||
auth.TryGetValue("DeviceId", out deviceId);
|
||||
auth.TryGetValue("Device", out device);
|
||||
auth.TryGetValue("Client", out client);
|
||||
auth.TryGetValue("Version", out version);
|
||||
}
|
||||
|
||||
return new AuthorizationInfo
|
||||
{
|
||||
Client = client,
|
||||
Device = device,
|
||||
DeviceId = deviceId,
|
||||
UserId = userId,
|
||||
Version = version
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authorization.
|
||||
/// </summary>
|
||||
/// <param name="authorizationHeader">The authorization header.</param>
|
||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||
private static Dictionary<string, string> GetAuthorization(string authorizationHeader)
|
||||
{
|
||||
if (authorizationHeader == null) return null;
|
||||
|
||||
var parts = authorizationHeader.Split(' ');
|
||||
|
||||
// There should be at least to parts
|
||||
if (parts.Length < 2) return null;
|
||||
|
||||
// It has to be a digest request
|
||||
if (!string.Equals(parts[0], "MediaBrowser", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove uptil the first space
|
||||
authorizationHeader = authorizationHeader.Substring(authorizationHeader.IndexOf(' '));
|
||||
parts = authorizationHeader.Split(',');
|
||||
|
||||
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var item in parts)
|
||||
{
|
||||
var param = item.Trim().Split(new[] { '=' }, 2);
|
||||
result.Add(param[0], param[1].Trim(new[] { '"' }));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A new shallow copy of this filter is used on every request.
|
||||
/// </summary>
|
||||
/// <returns>IHasRequestFilter.</returns>
|
||||
public IHasRequestFilter Copy()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Order in which Request Filters are executed.
|
||||
/// <0 Executed before global request filters
|
||||
/// >0 Executed after global request filters
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public int Priority
|
||||
{
|
||||
get { return 0; }
|
||||
}
|
||||
}
|
||||
|
||||
public class AuthorizationInfo
|
||||
{
|
||||
public string UserId;
|
||||
public string DeviceId;
|
||||
public string Device;
|
||||
public string Client;
|
||||
public string Version;
|
||||
}
|
||||
}
|
@ -14,8 +14,7 @@ namespace MediaBrowser.Api
|
||||
/// <summary>
|
||||
/// Class BaseApiService
|
||||
/// </summary>
|
||||
[AuthorizationRequestFilter]
|
||||
public class BaseApiService : IHasResultFactory, IRestfulService
|
||||
public class BaseApiService : IHasResultFactory, IRestfulService, IHasSession
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the logger.
|
||||
@ -35,6 +34,8 @@ namespace MediaBrowser.Api
|
||||
/// <value>The request context.</value>
|
||||
public IRequest Request { get; set; }
|
||||
|
||||
public ISessionContext SessionContext { get; set; }
|
||||
|
||||
public string GetHeader(string name)
|
||||
{
|
||||
return Request.Headers[name];
|
||||
@ -82,33 +83,18 @@ namespace MediaBrowser.Api
|
||||
/// <summary>
|
||||
/// Gets the session.
|
||||
/// </summary>
|
||||
/// <param name="sessionManager">The session manager.</param>
|
||||
/// <returns>SessionInfo.</returns>
|
||||
protected SessionInfo GetSession(ISessionManager sessionManager)
|
||||
/// <exception cref="System.ArgumentException">Session not found.</exception>
|
||||
protected SessionInfo GetSession()
|
||||
{
|
||||
var auth = AuthorizationRequestFilterAttribute.GetAuthorization(Request);
|
||||
var session = SessionContext.GetSession(Request);
|
||||
|
||||
return sessionManager.Sessions.First(i => string.Equals(i.DeviceId, auth.DeviceId) &&
|
||||
string.Equals(i.Client, auth.Client) &&
|
||||
string.Equals(i.ApplicationVersion, auth.Version));
|
||||
}
|
||||
if (session == null)
|
||||
{
|
||||
throw new ArgumentException("Session not found.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// To the cached result.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="cacheKey">The cache key.</param>
|
||||
/// <param name="lastDateModified">The last date modified.</param>
|
||||
/// <param name="cacheDuration">Duration of the cache.</param>
|
||||
/// <param name="factoryFn">The factory fn.</param>
|
||||
/// <param name="contentType">Type of the content.</param>
|
||||
/// <param name="responseHeaders">The response headers.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">cacheKey</exception>
|
||||
protected object ToCachedResult<T>(Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration, Func<T> factoryFn, string contentType, IDictionary<string,string> responseHeaders = null)
|
||||
where T : class
|
||||
{
|
||||
return ResultFactory.GetCachedResult(Request, cacheKey, lastDateModified, cacheDuration, factoryFn, contentType, responseHeaders);
|
||||
return session;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -121,7 +107,7 @@ namespace MediaBrowser.Api
|
||||
return ResultFactory.GetStaticFileResult(Request, path);
|
||||
}
|
||||
|
||||
private readonly char[] _dashReplaceChars = new[] { '?', '/' };
|
||||
private readonly char[] _dashReplaceChars = { '?', '/' };
|
||||
private const char SlugChar = '-';
|
||||
|
||||
protected MusicArtist GetArtist(string name, ILibraryManager libraryManager)
|
||||
@ -154,7 +140,7 @@ namespace MediaBrowser.Api
|
||||
return libraryManager.GetPerson(DeSlugPersonName(name, libraryManager));
|
||||
}
|
||||
|
||||
protected IList<BaseItem> GetAllLibraryItems(Guid? userId, IUserManager userManager, ILibraryManager libraryManager, string parentId = null)
|
||||
protected IEnumerable<BaseItem> GetAllLibraryItems(Guid? userId, IUserManager userManager, ILibraryManager libraryManager, string parentId = null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(parentId))
|
||||
{
|
||||
@ -164,7 +150,12 @@ namespace MediaBrowser.Api
|
||||
{
|
||||
var user = userManager.GetUserById(userId.Value);
|
||||
|
||||
return folder.GetRecursiveChildren(user).ToList();
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentException("User not found");
|
||||
}
|
||||
|
||||
return folder.GetRecursiveChildren(user);
|
||||
}
|
||||
|
||||
return folder.GetRecursiveChildren();
|
||||
@ -173,7 +164,12 @@ namespace MediaBrowser.Api
|
||||
{
|
||||
var user = userManager.GetUserById(userId.Value);
|
||||
|
||||
return userManager.GetUserById(userId.Value).RootFolder.GetRecursiveChildren(user, null);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentException("User not found");
|
||||
}
|
||||
|
||||
return userManager.GetUserById(userId.Value).RootFolder.GetRecursiveChildren(user);
|
||||
}
|
||||
|
||||
return libraryManager.RootFolder.GetRecursiveChildren();
|
||||
@ -234,7 +230,8 @@ namespace MediaBrowser.Api
|
||||
return name;
|
||||
}
|
||||
|
||||
return libraryManager.RootFolder.GetRecursiveChildren(i => i is Game)
|
||||
return libraryManager.RootFolder.GetRecursiveChildren()
|
||||
.OfType<Game>()
|
||||
.SelectMany(i => i.Genres)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(i =>
|
||||
|
28
MediaBrowser.Api/BrandingService.cs
Normal file
28
MediaBrowser.Api/BrandingService.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Model.Branding;
|
||||
using ServiceStack;
|
||||
|
||||
namespace MediaBrowser.Api
|
||||
{
|
||||
[Route("/Branding/Configuration", "GET", Summary = "Gets branding configuration")]
|
||||
public class GetBrandingOptions : IReturn<BrandingOptions>
|
||||
{
|
||||
}
|
||||
|
||||
public class BrandingService : BaseApiService
|
||||
{
|
||||
private readonly IConfigurationManager _config;
|
||||
|
||||
public BrandingService(IConfigurationManager config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public object Get(GetBrandingOptions request)
|
||||
{
|
||||
var result = _config.GetConfiguration<BrandingOptions>("branding");
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Channels;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
@ -8,6 +9,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Api
|
||||
{
|
||||
@ -173,6 +175,7 @@ namespace MediaBrowser.Api
|
||||
public string UserId { get; set; }
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class ChannelService : BaseApiService
|
||||
{
|
||||
private readonly IChannelManager _channelManager;
|
||||
@ -196,14 +199,14 @@ namespace MediaBrowser.Api
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public object Get(GetChannelFolder request)
|
||||
public async Task<object> Get(GetChannelFolder request)
|
||||
{
|
||||
return ToOptimizedResult(_channelManager.GetChannelFolder(request.UserId, CancellationToken.None).Result);
|
||||
return ToOptimizedResult(await _channelManager.GetChannelFolder(request.UserId, CancellationToken.None).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
public object Get(GetChannels request)
|
||||
public async Task<object> Get(GetChannels request)
|
||||
{
|
||||
var result = _channelManager.GetChannels(new ChannelQuery
|
||||
var result = await _channelManager.GetChannels(new ChannelQuery
|
||||
{
|
||||
Limit = request.Limit,
|
||||
StartIndex = request.StartIndex,
|
||||
@ -211,14 +214,14 @@ namespace MediaBrowser.Api
|
||||
SupportsLatestItems = request.SupportsLatestItems,
|
||||
IsFavorite = request.IsFavorite
|
||||
|
||||
}, CancellationToken.None).Result;
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public object Get(GetChannelItems request)
|
||||
public async Task<object> Get(GetChannelItems request)
|
||||
{
|
||||
var result = _channelManager.GetChannelItems(new ChannelItemQuery
|
||||
var result = await _channelManager.GetChannelItems(new ChannelItemQuery
|
||||
{
|
||||
Limit = request.Limit,
|
||||
StartIndex = request.StartIndex,
|
||||
@ -228,16 +231,16 @@ namespace MediaBrowser.Api
|
||||
SortOrder = request.SortOrder,
|
||||
SortBy = (request.SortBy ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(),
|
||||
Filters = request.GetFilters().ToArray(),
|
||||
Fields = request.GetItemFields().ToList()
|
||||
Fields = request.GetItemFields().ToArray()
|
||||
|
||||
}, CancellationToken.None).Result;
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public object Get(GetLatestChannelItems request)
|
||||
public async Task<object> Get(GetLatestChannelItems request)
|
||||
{
|
||||
var result = _channelManager.GetLatestChannelItems(new AllChannelMediaQuery
|
||||
var result = await _channelManager.GetLatestChannelItems(new AllChannelMediaQuery
|
||||
{
|
||||
Limit = request.Limit,
|
||||
StartIndex = request.StartIndex,
|
||||
@ -246,7 +249,7 @@ namespace MediaBrowser.Api
|
||||
Filters = request.GetFilters().ToArray(),
|
||||
Fields = request.GetItemFields().ToList()
|
||||
|
||||
}, CancellationToken.None).Result;
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
@ -1,15 +1,17 @@
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using ServiceStack;
|
||||
using ServiceStack.Text.Controller;
|
||||
using ServiceStack.Web;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Api
|
||||
@ -18,35 +20,58 @@ namespace MediaBrowser.Api
|
||||
/// Class GetConfiguration
|
||||
/// </summary>
|
||||
[Route("/System/Configuration", "GET", Summary = "Gets application configuration")]
|
||||
[Authenticated]
|
||||
public class GetConfiguration : IReturn<ServerConfiguration>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[Route("/System/Configuration/{Key}", "GET", Summary = "Gets a named configuration")]
|
||||
[Authenticated]
|
||||
public class GetNamedConfiguration
|
||||
{
|
||||
[ApiMember(Name = "Key", Description = "Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Key { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class UpdateConfiguration
|
||||
/// </summary>
|
||||
[Route("/System/Configuration", "POST", Summary = "Updates application configuration")]
|
||||
[Authenticated]
|
||||
public class UpdateConfiguration : ServerConfiguration, IReturnVoid
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/System/Configuration/{Key}", "POST", Summary = "Updates named configuration")]
|
||||
[Authenticated]
|
||||
public class UpdateNamedConfiguration : IReturnVoid, IRequiresRequestStream
|
||||
{
|
||||
[ApiMember(Name = "Key", Description = "Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Key { get; set; }
|
||||
|
||||
public Stream RequestStream { get; set; }
|
||||
}
|
||||
|
||||
[Route("/System/Configuration/MetadataOptions/Default", "GET", Summary = "Gets a default MetadataOptions object")]
|
||||
[Authenticated]
|
||||
public class GetDefaultMetadataOptions : IReturn<MetadataOptions>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[Route("/System/Configuration/MetadataPlugins", "GET", Summary = "Gets all available metadata plugins")]
|
||||
[Authenticated]
|
||||
public class GetMetadataPlugins : IReturn<List<MetadataPluginSummary>>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[Route("/System/Configuration/VideoImageExtraction", "POST", Summary = "Updates image extraction for all types")]
|
||||
public class UpdateVideoImageExtraction : IReturnVoid
|
||||
[Route("/System/Configuration/MetadataPlugins/Autoset", "POST")]
|
||||
[Authenticated]
|
||||
public class AutoSetMetadataOptions : IReturnVoid
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class ConfigurationService : BaseApiService
|
||||
@ -63,13 +88,15 @@ namespace MediaBrowser.Api
|
||||
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
public ConfigurationService(IJsonSerializer jsonSerializer, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IProviderManager providerManager)
|
||||
public ConfigurationService(IJsonSerializer jsonSerializer, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IProviderManager providerManager, ILibraryManager libraryManager)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_configurationManager = configurationManager;
|
||||
_fileSystem = fileSystem;
|
||||
_providerManager = providerManager;
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -88,6 +115,60 @@ namespace MediaBrowser.Api
|
||||
return ToOptimizedResultUsingCache(cacheKey, dateModified, null, () => _configurationManager.Configuration);
|
||||
}
|
||||
|
||||
public object Get(GetNamedConfiguration request)
|
||||
{
|
||||
var result = _configurationManager.GetConfiguration(request.Key);
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
const string XbmcMetadata = "Xbmc Nfo";
|
||||
const string MediaBrowserMetadata = "Media Browser Xml";
|
||||
|
||||
public void Post(AutoSetMetadataOptions request)
|
||||
{
|
||||
var service = AutoDetectMetadataService();
|
||||
|
||||
Logger.Info("Setting preferred metadata format to " + service);
|
||||
|
||||
var serviceToDisable = string.Equals(service, XbmcMetadata) ?
|
||||
MediaBrowserMetadata :
|
||||
XbmcMetadata;
|
||||
|
||||
_configurationManager.DisableMetadataService(serviceToDisable);
|
||||
_configurationManager.SaveConfiguration();
|
||||
}
|
||||
|
||||
private string AutoDetectMetadataService()
|
||||
{
|
||||
try
|
||||
{
|
||||
var paths = _libraryManager.GetDefaultVirtualFolders()
|
||||
.SelectMany(i => i.Locations)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Select(i => new DirectoryInfo(i))
|
||||
.ToList();
|
||||
|
||||
if (paths.SelectMany(i => i.EnumerateFiles("*.xml", SearchOption.AllDirectories))
|
||||
.Any())
|
||||
{
|
||||
return XbmcMetadata;
|
||||
}
|
||||
|
||||
if (paths.SelectMany(i => i.EnumerateFiles("*.xml", SearchOption.AllDirectories))
|
||||
.Any(i => string.Equals(i.Name, "series.xml", StringComparison.OrdinalIgnoreCase) || string.Equals(i.Name, "movie.xml", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return MediaBrowserMetadata;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
return XbmcMetadata;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified configuraiton.
|
||||
/// </summary>
|
||||
@ -95,7 +176,6 @@ namespace MediaBrowser.Api
|
||||
public void Post(UpdateConfiguration request)
|
||||
{
|
||||
// Silly, but we need to serialize and deserialize or the XmlSerializer will write the xml with an element name of UpdateConfiguration
|
||||
|
||||
var json = _jsonSerializer.SerializeToString(request);
|
||||
|
||||
var config = _jsonSerializer.DeserializeFromString<ServerConfiguration>(json);
|
||||
@ -103,6 +183,17 @@ namespace MediaBrowser.Api
|
||||
_configurationManager.ReplaceConfiguration(config);
|
||||
}
|
||||
|
||||
public void Post(UpdateNamedConfiguration request)
|
||||
{
|
||||
var pathInfo = PathInfo.Parse(Request.PathInfo);
|
||||
var key = pathInfo.GetArgumentValue<string>(2);
|
||||
|
||||
var configurationType = _configurationManager.GetConfigurationType(key);
|
||||
var configuration = _jsonSerializer.DeserializeFromStream(request.RequestStream, configurationType);
|
||||
|
||||
_configurationManager.SaveConfiguration(key, configuration);
|
||||
}
|
||||
|
||||
public object Get(GetDefaultMetadataOptions request)
|
||||
{
|
||||
return ToOptimizedSerializedResultUsingCache(new MetadataOptions());
|
||||
@ -112,71 +203,5 @@ namespace MediaBrowser.Api
|
||||
{
|
||||
return ToOptimizedSerializedResultUsingCache(_providerManager.GetAllMetadataPlugins().ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is a temporary method used until image settings get broken out.
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
public void Post(UpdateVideoImageExtraction request)
|
||||
{
|
||||
var config = _configurationManager.Configuration;
|
||||
|
||||
EnableImageExtractionForType(typeof(Movie), config, request.Enabled);
|
||||
EnableImageExtractionForType(typeof(Episode), config, request.Enabled);
|
||||
EnableImageExtractionForType(typeof(AdultVideo), config, request.Enabled);
|
||||
EnableImageExtractionForType(typeof(MusicVideo), config, request.Enabled);
|
||||
EnableImageExtractionForType(typeof(Video), config, request.Enabled);
|
||||
EnableImageExtractionForType(typeof(Trailer), config, request.Enabled);
|
||||
|
||||
_configurationManager.SaveConfiguration();
|
||||
}
|
||||
|
||||
private void EnableImageExtractionForType(Type type, ServerConfiguration config, bool enabled)
|
||||
{
|
||||
var options = GetMetadataOptions(type, config);
|
||||
|
||||
const string imageProviderName = "Screen Grabber";
|
||||
|
||||
var contains = options.DisabledImageFetchers.Contains(imageProviderName, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (!enabled && !contains)
|
||||
{
|
||||
var list = options.DisabledImageFetchers.ToList();
|
||||
|
||||
list.Add(imageProviderName);
|
||||
|
||||
options.DisabledImageFetchers = list.ToArray();
|
||||
}
|
||||
else if (enabled && contains)
|
||||
{
|
||||
var list = options.DisabledImageFetchers.ToList();
|
||||
|
||||
list.Remove(imageProviderName);
|
||||
|
||||
options.DisabledImageFetchers = list.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private MetadataOptions GetMetadataOptions(Type type, ServerConfiguration config)
|
||||
{
|
||||
var options = config.MetadataOptions
|
||||
.FirstOrDefault(i => string.Equals(i.ItemType, type.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (options == null)
|
||||
{
|
||||
var list = config.MetadataOptions.ToList();
|
||||
|
||||
options = new MetadataOptions
|
||||
{
|
||||
ItemType = type.Name
|
||||
};
|
||||
|
||||
list.Add(options);
|
||||
|
||||
config.MetadataOptions = list.ToArray();
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,671 +0,0 @@
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Api.DefaultTheme
|
||||
{
|
||||
[Route("/MBT/DefaultTheme/Games", "GET")]
|
||||
public class GetGamesView : IReturn<GamesView>
|
||||
{
|
||||
[ApiMember(Name = "UserId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
[ApiMember(Name = "RecentlyPlayedGamesLimit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int RecentlyPlayedGamesLimit { get; set; }
|
||||
|
||||
public string ParentId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/MBT/DefaultTheme/TV", "GET")]
|
||||
public class GetTvView : IReturn<TvView>
|
||||
{
|
||||
[ApiMember(Name = "UserId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
[ApiMember(Name = "ComedyGenre", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
|
||||
public string ComedyGenre { get; set; }
|
||||
|
||||
[ApiMember(Name = "RomanceGenre", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
|
||||
public string RomanceGenre { get; set; }
|
||||
|
||||
[ApiMember(Name = "TopCommunityRating", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public double TopCommunityRating { get; set; }
|
||||
|
||||
[ApiMember(Name = "NextUpEpisodeLimit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int NextUpEpisodeLimit { get; set; }
|
||||
|
||||
[ApiMember(Name = "ResumableEpisodeLimit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int ResumableEpisodeLimit { get; set; }
|
||||
|
||||
[ApiMember(Name = "LatestEpisodeLimit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int LatestEpisodeLimit { get; set; }
|
||||
|
||||
public string ParentId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/MBT/DefaultTheme/Movies", "GET")]
|
||||
public class GetMovieView : IReturn<MoviesView>
|
||||
{
|
||||
[ApiMember(Name = "UserId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
[ApiMember(Name = "FamilyGenre", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
|
||||
public string FamilyGenre { get; set; }
|
||||
|
||||
[ApiMember(Name = "ComedyGenre", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
|
||||
public string ComedyGenre { get; set; }
|
||||
|
||||
[ApiMember(Name = "RomanceGenre", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
|
||||
public string RomanceGenre { get; set; }
|
||||
|
||||
[ApiMember(Name = "LatestMoviesLimit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int LatestMoviesLimit { get; set; }
|
||||
|
||||
[ApiMember(Name = "LatestTrailersLimit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int LatestTrailersLimit { get; set; }
|
||||
|
||||
public string ParentId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/MBT/DefaultTheme/Favorites", "GET")]
|
||||
public class GetFavoritesView : IReturn<FavoritesView>
|
||||
{
|
||||
[ApiMember(Name = "UserId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public Guid UserId { get; set; }
|
||||
}
|
||||
|
||||
public class DefaultThemeService : BaseApiService
|
||||
{
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
|
||||
public DefaultThemeService(IUserManager userManager, IDtoService dtoService, ILogger logger, ILibraryManager libraryManager, IImageProcessor imageProcessor, IUserDataManager userDataManager, IItemRepository itemRepo)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_dtoService = dtoService;
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_imageProcessor = imageProcessor;
|
||||
_userDataManager = userDataManager;
|
||||
_itemRepo = itemRepo;
|
||||
}
|
||||
|
||||
public object Get(GetFavoritesView request)
|
||||
{
|
||||
var user = _userManager.GetUserById(request.UserId);
|
||||
|
||||
var allItems = user.RootFolder.GetRecursiveChildren(user)
|
||||
.ToList();
|
||||
|
||||
var allFavoriteItems = allItems.Where(i => _userDataManager.GetUserData(user.Id, i.GetUserDataKey()).IsFavorite)
|
||||
.ToList();
|
||||
|
||||
var itemsWithImages = allFavoriteItems.Where(i => !string.IsNullOrEmpty(i.PrimaryImagePath))
|
||||
.ToList();
|
||||
|
||||
var itemsWithBackdrops = allFavoriteItems.Where(i => i.GetImages(ImageType.Backdrop).Any())
|
||||
.ToList();
|
||||
|
||||
var view = new FavoritesView();
|
||||
|
||||
var fields = new List<ItemFields>();
|
||||
|
||||
view.BackdropItems = FilterItemsForBackdropDisplay(itemsWithBackdrops)
|
||||
.Randomize("backdrop")
|
||||
.Take(10)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
var spotlightItems = itemsWithBackdrops.Randomize("spotlight")
|
||||
.Take(10)
|
||||
.ToList();
|
||||
|
||||
view.SpotlightItems = spotlightItems
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
fields.Add(ItemFields.PrimaryImageAspectRatio);
|
||||
|
||||
view.Albums = itemsWithImages
|
||||
.OfType<MusicAlbum>()
|
||||
.Randomize()
|
||||
.Take(4)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
view.Books = itemsWithImages
|
||||
.OfType<Book>()
|
||||
.Randomize()
|
||||
.Take(6)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
view.Episodes = itemsWithImages
|
||||
.OfType<Episode>()
|
||||
.Randomize()
|
||||
.Take(6)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
view.Games = itemsWithImages
|
||||
.OfType<Game>()
|
||||
.Randomize()
|
||||
.Take(6)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
view.Movies = itemsWithImages
|
||||
.OfType<Movie>()
|
||||
.Randomize()
|
||||
.Take(6)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
view.Series = itemsWithImages
|
||||
.OfType<Series>()
|
||||
.Randomize()
|
||||
.Take(6)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
view.Songs = itemsWithImages
|
||||
.OfType<Audio>()
|
||||
.Randomize()
|
||||
.Take(4)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
view.MiniSpotlights = itemsWithBackdrops
|
||||
.Except(spotlightItems)
|
||||
.Randomize()
|
||||
.Take(5)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
var artists = allItems.OfType<Audio>()
|
||||
.SelectMany(i => i.AllArtists)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Randomize()
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return _libraryManager.GetArtist(i);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.Where(i => i != null && _userDataManager.GetUserData(user.Id, i.GetUserDataKey()).IsFavorite)
|
||||
.Take(4)
|
||||
.ToList();
|
||||
|
||||
view.Artists = artists
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(view);
|
||||
}
|
||||
|
||||
public object Get(GetGamesView request)
|
||||
{
|
||||
var user = _userManager.GetUserById(request.UserId);
|
||||
|
||||
var items = GetAllLibraryItems(user.Id, _userManager, _libraryManager, request.ParentId).Where(i => i is Game || i is GameSystem)
|
||||
.ToList();
|
||||
|
||||
var gamesWithImages = items.OfType<Game>().Where(i => !string.IsNullOrEmpty(i.PrimaryImagePath)).ToList();
|
||||
|
||||
var itemsWithBackdrops = FilterItemsForBackdropDisplay(items.Where(i => i.GetImages(ImageType.Backdrop).Any())).ToList();
|
||||
|
||||
var gamesWithBackdrops = itemsWithBackdrops.OfType<Game>().ToList();
|
||||
|
||||
var view = new GamesView();
|
||||
|
||||
var fields = new List<ItemFields>();
|
||||
|
||||
view.GameSystems = items
|
||||
.OfType<GameSystem>()
|
||||
.OrderBy(i => i.SortName)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
var currentUserId = user.Id;
|
||||
view.RecentlyPlayedGames = gamesWithImages
|
||||
.OrderByDescending(i => _userDataManager.GetUserData(currentUserId, i.GetUserDataKey()).LastPlayedDate ?? DateTime.MinValue)
|
||||
.Take(request.RecentlyPlayedGamesLimit)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
view.BackdropItems = gamesWithBackdrops
|
||||
.OrderBy(i => Guid.NewGuid())
|
||||
.Take(10)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
view.SpotlightItems = gamesWithBackdrops
|
||||
.OrderBy(i => Guid.NewGuid())
|
||||
.Take(10)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
view.MultiPlayerItems = gamesWithImages
|
||||
.Where(i => i.PlayersSupported.HasValue && i.PlayersSupported.Value > 1)
|
||||
.Randomize()
|
||||
.Select(i => GetItemStub(i, ImageType.Primary))
|
||||
.Where(i => i != null)
|
||||
.Take(1)
|
||||
.ToList();
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(view);
|
||||
}
|
||||
|
||||
public object Get(GetTvView request)
|
||||
{
|
||||
var romanceGenres = request.RomanceGenre.Split(',').ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
|
||||
var comedyGenres = request.ComedyGenre.Split(',').ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var user = _userManager.GetUserById(request.UserId);
|
||||
|
||||
var series = GetAllLibraryItems(user.Id, _userManager, _libraryManager, request.ParentId)
|
||||
.OfType<Series>()
|
||||
.ToList();
|
||||
|
||||
var seriesWithBackdrops = series.Where(i => i.GetImages(ImageType.Backdrop).Any()).ToList();
|
||||
|
||||
var view = new TvView();
|
||||
|
||||
var fields = new List<ItemFields>();
|
||||
|
||||
var seriesWithBestBackdrops = FilterItemsForBackdropDisplay(seriesWithBackdrops).ToList();
|
||||
|
||||
view.BackdropItems = seriesWithBestBackdrops
|
||||
.OrderBy(i => Guid.NewGuid())
|
||||
.Take(10)
|
||||
.AsParallel()
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
view.ShowsItems = series
|
||||
.Where(i => i.GetImages(ImageType.Backdrop).Any())
|
||||
.Randomize("all")
|
||||
.Select(i => GetItemStub(i, ImageType.Backdrop))
|
||||
.Where(i => i != null)
|
||||
.Take(1)
|
||||
.ToList();
|
||||
|
||||
view.RomanceItems = seriesWithBackdrops
|
||||
.Where(i => i.Genres.Any(romanceGenres.ContainsKey))
|
||||
.Randomize("romance")
|
||||
.Select(i => GetItemStub(i, ImageType.Backdrop))
|
||||
.Where(i => i != null)
|
||||
.Take(1)
|
||||
.ToList();
|
||||
|
||||
view.ComedyItems = seriesWithBackdrops
|
||||
.Where(i => i.Genres.Any(comedyGenres.ContainsKey))
|
||||
.Randomize("comedy")
|
||||
.Select(i => GetItemStub(i, ImageType.Backdrop))
|
||||
.Where(i => i != null)
|
||||
.Take(1)
|
||||
.ToList();
|
||||
|
||||
var spotlightSeries = seriesWithBestBackdrops
|
||||
.Where(i => i.CommunityRating.HasValue && i.CommunityRating >= 8.5)
|
||||
.ToList();
|
||||
|
||||
if (spotlightSeries.Count < 20)
|
||||
{
|
||||
spotlightSeries = seriesWithBestBackdrops;
|
||||
}
|
||||
|
||||
spotlightSeries = spotlightSeries
|
||||
.OrderBy(i => Guid.NewGuid())
|
||||
.Take(10)
|
||||
.ToList();
|
||||
|
||||
view.SpotlightItems = spotlightSeries
|
||||
.AsParallel()
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
var miniSpotlightItems = seriesWithBackdrops
|
||||
.Except(spotlightSeries.OfType<Series>())
|
||||
.Where(i => i.CommunityRating.HasValue && i.CommunityRating >= 8)
|
||||
.ToList();
|
||||
|
||||
if (miniSpotlightItems.Count < 15)
|
||||
{
|
||||
miniSpotlightItems = seriesWithBackdrops;
|
||||
}
|
||||
|
||||
view.MiniSpotlights = miniSpotlightItems
|
||||
.Randomize("minispotlight")
|
||||
.Take(5)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
var nextUpEpisodes = new TvShowsService(_userManager, _userDataManager, _libraryManager, _itemRepo, _dtoService)
|
||||
.GetNextUpEpisodes(new GetNextUpEpisodes { UserId = user.Id }, series)
|
||||
.ToList();
|
||||
|
||||
fields.Add(ItemFields.PrimaryImageAspectRatio);
|
||||
|
||||
view.NextUpEpisodes = nextUpEpisodes
|
||||
.Take(request.NextUpEpisodeLimit)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
view.SeriesIdsInProgress = nextUpEpisodes.Select(i => i.Series.Id.ToString("N")).ToList();
|
||||
|
||||
// Avoid implicitly captured closure
|
||||
var currentUser1 = user;
|
||||
|
||||
var ownedEpisodes = series
|
||||
.SelectMany(i => i.GetRecursiveChildren(currentUser1, j => j.LocationType != LocationType.Virtual))
|
||||
.OfType<Episode>()
|
||||
.ToList();
|
||||
|
||||
// Avoid implicitly captured closure
|
||||
var currentUser = user;
|
||||
|
||||
view.LatestEpisodes = ownedEpisodes
|
||||
.OrderByDescending(i => i.DateCreated)
|
||||
.Where(i => !_userDataManager.GetUserData(currentUser.Id, i.GetUserDataKey()).Played)
|
||||
.Take(request.LatestEpisodeLimit)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
view.ResumableEpisodes = ownedEpisodes
|
||||
.Where(i => _userDataManager.GetUserData(currentUser.Id, i.GetUserDataKey()).PlaybackPositionTicks > 0)
|
||||
.OrderByDescending(i => _userDataManager.GetUserData(currentUser.Id, i.GetUserDataKey()).LastPlayedDate ?? DateTime.MinValue)
|
||||
.Take(request.ResumableEpisodeLimit)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(view);
|
||||
}
|
||||
|
||||
public object Get(GetMovieView request)
|
||||
{
|
||||
var user = _userManager.GetUserById(request.UserId);
|
||||
|
||||
var items = GetAllLibraryItems(user.Id, _userManager, _libraryManager, request.ParentId)
|
||||
.Where(i => i is Movie || i is Trailer || i is BoxSet)
|
||||
.ToList();
|
||||
|
||||
var view = new MoviesView();
|
||||
|
||||
var movies = items.OfType<Movie>()
|
||||
.ToList();
|
||||
|
||||
var trailers = items.OfType<Trailer>()
|
||||
.ToList();
|
||||
|
||||
var hdMovies = movies.Where(i => i.IsHD).ToList();
|
||||
|
||||
var familyGenres = request.FamilyGenre.Split(',').ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var familyMovies = movies.Where(i => i.Genres.Any(familyGenres.ContainsKey)).ToList();
|
||||
|
||||
view.HDMoviePercentage = 100 * hdMovies.Count;
|
||||
view.HDMoviePercentage /= movies.Count;
|
||||
|
||||
view.FamilyMoviePercentage = 100 * familyMovies.Count;
|
||||
view.FamilyMoviePercentage /= movies.Count;
|
||||
|
||||
var moviesWithBackdrops = movies
|
||||
.Where(i => i.GetImages(ImageType.Backdrop).Any())
|
||||
.ToList();
|
||||
|
||||
var fields = new List<ItemFields>();
|
||||
|
||||
var itemsWithTopBackdrops = FilterItemsForBackdropDisplay(moviesWithBackdrops).ToList();
|
||||
|
||||
view.BackdropItems = itemsWithTopBackdrops
|
||||
.OrderBy(i => Guid.NewGuid())
|
||||
.Take(10)
|
||||
.AsParallel()
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
view.MovieItems = moviesWithBackdrops
|
||||
.Randomize("all")
|
||||
.Select(i => GetItemStub(i, ImageType.Backdrop))
|
||||
.Where(i => i != null)
|
||||
.Take(1)
|
||||
.ToList();
|
||||
|
||||
view.TrailerItems = trailers
|
||||
.Where(i => !string.IsNullOrEmpty(i.PrimaryImagePath))
|
||||
.Randomize()
|
||||
.Select(i => GetItemStub(i, ImageType.Primary))
|
||||
.Where(i => i != null)
|
||||
.Take(1)
|
||||
.ToList();
|
||||
|
||||
view.BoxSetItems = items
|
||||
.OfType<BoxSet>()
|
||||
.Where(i => i.GetImages(ImageType.Backdrop).Any())
|
||||
.Randomize()
|
||||
.Select(i => GetItemStub(i, ImageType.Backdrop))
|
||||
.Where(i => i != null)
|
||||
.Take(1)
|
||||
.ToList();
|
||||
|
||||
view.ThreeDItems = moviesWithBackdrops
|
||||
.Where(i => i.Is3D)
|
||||
.Randomize("3d")
|
||||
.Select(i => GetItemStub(i, ImageType.Backdrop))
|
||||
.Where(i => i != null)
|
||||
.Take(1)
|
||||
.ToList();
|
||||
|
||||
var romanceGenres = request.RomanceGenre.Split(',').ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
|
||||
var comedyGenres = request.ComedyGenre.Split(',').ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
view.RomanceItems = moviesWithBackdrops
|
||||
.Where(i => i.Genres.Any(romanceGenres.ContainsKey))
|
||||
.Randomize("romance")
|
||||
.Select(i => GetItemStub(i, ImageType.Backdrop))
|
||||
.Where(i => i != null)
|
||||
.Take(1)
|
||||
.ToList();
|
||||
|
||||
view.ComedyItems = moviesWithBackdrops
|
||||
.Where(i => i.Genres.Any(comedyGenres.ContainsKey))
|
||||
.Randomize("comedy")
|
||||
.Select(i => GetItemStub(i, ImageType.Backdrop))
|
||||
.Where(i => i != null)
|
||||
.Take(1)
|
||||
.ToList();
|
||||
|
||||
view.HDItems = hdMovies
|
||||
.Where(i => i.GetImages(ImageType.Backdrop).Any())
|
||||
.Randomize("hd")
|
||||
.Select(i => GetItemStub(i, ImageType.Backdrop))
|
||||
.Where(i => i != null)
|
||||
.Take(1)
|
||||
.ToList();
|
||||
|
||||
view.FamilyMovies = familyMovies
|
||||
.Where(i => i.GetImages(ImageType.Backdrop).Any())
|
||||
.Randomize("family")
|
||||
.Select(i => GetItemStub(i, ImageType.Backdrop))
|
||||
.Where(i => i != null)
|
||||
.Take(1)
|
||||
.ToList();
|
||||
|
||||
var currentUserId = user.Id;
|
||||
var spotlightItems = itemsWithTopBackdrops
|
||||
.Where(i => i.CommunityRating.HasValue && i.CommunityRating >= 8)
|
||||
.Where(i => !_userDataManager.GetUserData(currentUserId, i.GetUserDataKey()).Played)
|
||||
.ToList();
|
||||
|
||||
if (spotlightItems.Count < 20)
|
||||
{
|
||||
spotlightItems = itemsWithTopBackdrops;
|
||||
}
|
||||
|
||||
spotlightItems = spotlightItems
|
||||
.OrderBy(i => Guid.NewGuid())
|
||||
.Take(10)
|
||||
.ToList();
|
||||
|
||||
view.SpotlightItems = spotlightItems
|
||||
.AsParallel()
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
var miniSpotlightItems = moviesWithBackdrops
|
||||
.Except(spotlightItems)
|
||||
.Where(i => i.CommunityRating.HasValue && i.CommunityRating >= 7.5)
|
||||
.ToList();
|
||||
|
||||
if (miniSpotlightItems.Count < 15)
|
||||
{
|
||||
miniSpotlightItems = itemsWithTopBackdrops;
|
||||
}
|
||||
|
||||
miniSpotlightItems = miniSpotlightItems
|
||||
.Randomize("minispotlight")
|
||||
.ToList();
|
||||
|
||||
// Avoid implicitly captured closure
|
||||
miniSpotlightItems.InsertRange(0, moviesWithBackdrops
|
||||
.Where(i => _userDataManager.GetUserData(currentUserId, i.GetUserDataKey()).PlaybackPositionTicks > 0)
|
||||
.OrderByDescending(i => _userDataManager.GetUserData(currentUserId, i.GetUserDataKey()).LastPlayedDate ?? DateTime.MaxValue)
|
||||
.Take(3));
|
||||
|
||||
view.MiniSpotlights = miniSpotlightItems
|
||||
.Take(3)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
// Avoid implicitly captured closure
|
||||
var currentUserId1 = user.Id;
|
||||
|
||||
view.LatestMovies = movies
|
||||
.OrderByDescending(i => i.DateCreated)
|
||||
.Where(i => !_userDataManager.GetUserData(currentUserId1, i.GetUserDataKey()).Played)
|
||||
.Take(request.LatestMoviesLimit)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
view.LatestTrailers = trailers
|
||||
.OrderByDescending(i => i.DateCreated)
|
||||
.Where(i => !_userDataManager.GetUserData(currentUserId1, i.GetUserDataKey()).Played)
|
||||
.Take(request.LatestTrailersLimit)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToList();
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(view);
|
||||
}
|
||||
|
||||
private IEnumerable<BaseItem> FilterItemsForBackdropDisplay(IEnumerable<BaseItem> items)
|
||||
{
|
||||
var tuples = items
|
||||
.Select(i => new Tuple<BaseItem, double>(i, GetResolution(i, ImageType.Backdrop, 0)))
|
||||
.Where(i => i.Item2 > 0)
|
||||
.ToList();
|
||||
|
||||
var topItems = tuples
|
||||
.Where(i => i.Item2 >= 1920)
|
||||
.ToList();
|
||||
|
||||
if (topItems.Count >= 10)
|
||||
{
|
||||
return topItems.Select(i => i.Item1);
|
||||
}
|
||||
|
||||
return tuples.Select(i => i.Item1);
|
||||
}
|
||||
|
||||
private double GetResolution(BaseItem item, ImageType type, int index)
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = item.GetImageInfo(type, index);
|
||||
|
||||
var size = _imageProcessor.GetImageSize(info.Path, info.DateModified);
|
||||
|
||||
return size.Width;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private ItemStub GetItemStub(BaseItem item, ImageType imageType)
|
||||
{
|
||||
var stub = new ItemStub
|
||||
{
|
||||
Id = _dtoService.GetDtoId(item),
|
||||
Name = item.Name,
|
||||
ImageType = imageType
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var tag = _imageProcessor.GetImageCacheTag(item, imageType);
|
||||
|
||||
if (tag != null)
|
||||
{
|
||||
stub.ImageTag = tag;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error getting image tag for {0}", ex, item.Path);
|
||||
return null;
|
||||
}
|
||||
|
||||
return stub;
|
||||
}
|
||||
}
|
||||
|
||||
static class RandomExtension
|
||||
{
|
||||
public static IEnumerable<T> Randomize<T>(this IEnumerable<T> sequence, string type = "none")
|
||||
where T : BaseItem
|
||||
{
|
||||
var hour = DateTime.Now.Hour + DateTime.Now.Day + 2;
|
||||
|
||||
var typeCode = type.GetHashCode();
|
||||
|
||||
return sequence.OrderBy(i =>
|
||||
{
|
||||
var val = i.Id.GetHashCode() + i.Genres.Count + i.People.Count + (i.ProductionYear ?? 0) + i.DateCreated.Minute + i.DateModified.Minute + typeCode;
|
||||
|
||||
return val % hour;
|
||||
});
|
||||
}
|
||||
|
||||
public static IEnumerable<string> Randomize(this IEnumerable<string> sequence)
|
||||
{
|
||||
var hour = DateTime.Now.Hour + 2;
|
||||
|
||||
return sequence.OrderBy(i => i.GetHashCode() % hour);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Api.DefaultTheme
|
||||
{
|
||||
public class ItemStub
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string ImageTag { get; set; }
|
||||
public ImageType ImageType { get; set; }
|
||||
}
|
||||
|
||||
public class MoviesView : BaseView
|
||||
{
|
||||
public List<ItemStub> MovieItems { get; set; }
|
||||
|
||||
public List<ItemStub> BoxSetItems { get; set; }
|
||||
public List<ItemStub> TrailerItems { get; set; }
|
||||
public List<ItemStub> HDItems { get; set; }
|
||||
public List<ItemStub> ThreeDItems { get; set; }
|
||||
|
||||
public List<ItemStub> FamilyMovies { get; set; }
|
||||
|
||||
public List<ItemStub> RomanceItems { get; set; }
|
||||
public List<ItemStub> ComedyItems { get; set; }
|
||||
|
||||
public double FamilyMoviePercentage { get; set; }
|
||||
|
||||
public double HDMoviePercentage { get; set; }
|
||||
|
||||
public List<BaseItemDto> LatestTrailers { get; set; }
|
||||
public List<BaseItemDto> LatestMovies { get; set; }
|
||||
}
|
||||
|
||||
public class TvView : BaseView
|
||||
{
|
||||
public List<ItemStub> ShowsItems { get; set; }
|
||||
|
||||
public List<ItemStub> RomanceItems { get; set; }
|
||||
public List<ItemStub> ComedyItems { get; set; }
|
||||
|
||||
public List<string> SeriesIdsInProgress { get; set; }
|
||||
|
||||
public List<BaseItemDto> LatestEpisodes { get; set; }
|
||||
public List<BaseItemDto> NextUpEpisodes { get; set; }
|
||||
public List<BaseItemDto> ResumableEpisodes { get; set; }
|
||||
}
|
||||
|
||||
public class ItemByNameInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int ItemCount { get; set; }
|
||||
}
|
||||
|
||||
public class GamesView : BaseView
|
||||
{
|
||||
public List<ItemStub> MultiPlayerItems { get; set; }
|
||||
public List<BaseItemDto> GameSystems { get; set; }
|
||||
public List<BaseItemDto> RecentlyPlayedGames { get; set; }
|
||||
}
|
||||
|
||||
public class BaseView
|
||||
{
|
||||
public List<BaseItemDto> BackdropItems { get; set; }
|
||||
public List<BaseItemDto> SpotlightItems { get; set; }
|
||||
public List<BaseItemDto> MiniSpotlights { get; set; }
|
||||
}
|
||||
|
||||
public class FavoritesView : BaseView
|
||||
{
|
||||
public List<BaseItemDto> Movies { get; set; }
|
||||
public List<BaseItemDto> Series { get; set; }
|
||||
public List<BaseItemDto> Episodes { get; set; }
|
||||
public List<BaseItemDto> Games { get; set; }
|
||||
public List<BaseItemDto> Books { get; set; }
|
||||
public List<BaseItemDto> Albums { get; set; }
|
||||
public List<BaseItemDto> Songs { get; set; }
|
||||
public List<BaseItemDto> Artists { get; set; }
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using ServiceStack;
|
||||
@ -48,6 +49,7 @@ namespace MediaBrowser.Api
|
||||
/// <summary>
|
||||
/// Class DisplayPreferencesService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class DisplayPreferencesService : BaseApiService
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -1,4 +1,6 @@
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using ServiceStack;
|
||||
using ServiceStack.Text.Controller;
|
||||
using ServiceStack.Web;
|
||||
@ -76,11 +78,14 @@ namespace MediaBrowser.Api.Dlna
|
||||
private readonly IContentDirectory _contentDirectory;
|
||||
private readonly IConnectionManager _connectionManager;
|
||||
|
||||
public DlnaServerService(IDlnaManager dlnaManager, IContentDirectory contentDirectory, IConnectionManager connectionManager)
|
||||
private readonly IConfigurationManager _config;
|
||||
|
||||
public DlnaServerService(IDlnaManager dlnaManager, IContentDirectory contentDirectory, IConnectionManager connectionManager, IConfigurationManager config)
|
||||
{
|
||||
_dlnaManager = dlnaManager;
|
||||
_contentDirectory = contentDirectory;
|
||||
_connectionManager = connectionManager;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public object Get(GetDescriptionXml request)
|
||||
@ -104,16 +109,16 @@ namespace MediaBrowser.Api.Dlna
|
||||
return ResultFactory.GetResult(xml, "text/xml");
|
||||
}
|
||||
|
||||
public object Post(ProcessContentDirectoryControlRequest request)
|
||||
public async Task<object> Post(ProcessContentDirectoryControlRequest request)
|
||||
{
|
||||
var response = PostAsync(request.RequestStream, _contentDirectory).Result;
|
||||
var response = await PostAsync(request.RequestStream, _contentDirectory).ConfigureAwait(false);
|
||||
|
||||
return ResultFactory.GetResult(response.Xml, "text/xml");
|
||||
}
|
||||
|
||||
public object Post(ProcessConnectionManagerControlRequest request)
|
||||
public async Task<object> Post(ProcessConnectionManagerControlRequest request)
|
||||
{
|
||||
var response = PostAsync(request.RequestStream, _connectionManager).Result;
|
||||
var response = await PostAsync(request.RequestStream, _connectionManager).ConfigureAwait(false);
|
||||
|
||||
return ResultFactory.GetResult(response.Xml, "text/xml");
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using ServiceStack;
|
||||
using System.Collections.Generic;
|
||||
@ -42,6 +43,7 @@ namespace MediaBrowser.Api.Dlna
|
||||
{
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class DlnaService : BaseApiService
|
||||
{
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
using ServiceStack;
|
||||
@ -86,6 +87,7 @@ namespace MediaBrowser.Api
|
||||
/// <summary>
|
||||
/// Class EnvironmentService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class EnvironmentService : BaseApiService
|
||||
{
|
||||
const char UncSeparator = '\\';
|
||||
|
@ -1,6 +1,7 @@
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using ServiceStack;
|
||||
@ -51,6 +52,7 @@ namespace MediaBrowser.Api
|
||||
/// <summary>
|
||||
/// Class GamesService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class GamesService : BaseApiService
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -1,4 +1,5 @@
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Dto;
|
||||
@ -100,13 +101,16 @@ namespace MediaBrowser.Api.Images
|
||||
/// </summary>
|
||||
private readonly IServerApplicationPaths _appPaths;
|
||||
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageByNameService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="appPaths">The app paths.</param>
|
||||
public ImageByNameService(IServerApplicationPaths appPaths)
|
||||
public ImageByNameService(IServerApplicationPaths appPaths, IFileSystem fileSystem)
|
||||
{
|
||||
_appPaths = appPaths;
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
public object Get(GetMediaInfoImages request)
|
||||
@ -133,7 +137,7 @@ namespace MediaBrowser.Api.Images
|
||||
.Where(i => BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.Ordinal))
|
||||
.Select(i => new ImageByNameInfo
|
||||
{
|
||||
Name = Path.GetFileNameWithoutExtension(i.FullName),
|
||||
Name = _fileSystem.GetFileNameWithoutExtension(i),
|
||||
FileLength = i.Length,
|
||||
|
||||
// For themeable images, use the Theme property
|
||||
|
@ -3,6 +3,7 @@ using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
@ -13,7 +14,6 @@ using ServiceStack.Text.Controller;
|
||||
using ServiceStack.Web;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@ -26,6 +26,7 @@ namespace MediaBrowser.Api.Images
|
||||
/// </summary>
|
||||
[Route("/Items/{Id}/Images", "GET")]
|
||||
[Api(Description = "Gets information about an item's images")]
|
||||
[Authenticated]
|
||||
public class GetItemImageInfos : IReturn<List<ImageInfo>>
|
||||
{
|
||||
/// <summary>
|
||||
@ -38,6 +39,8 @@ namespace MediaBrowser.Api.Images
|
||||
|
||||
[Route("/Items/{Id}/Images/{Type}", "GET")]
|
||||
[Route("/Items/{Id}/Images/{Type}/{Index}", "GET")]
|
||||
[Route("/Items/{Id}/Images/{Type}/{Index}/{Tag}/{Format}/{MaxWidth}/{MaxHeight}", "GET")]
|
||||
[Route("/Items/{Id}/Images/{Type}/{Index}/{Tag}/{Format}/{MaxWidth}/{MaxHeight}", "HEAD")]
|
||||
[Api(Description = "Gets an item image")]
|
||||
public class GetItemImage : ImageRequest
|
||||
{
|
||||
@ -47,8 +50,6 @@ namespace MediaBrowser.Api.Images
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Params { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -56,6 +57,7 @@ namespace MediaBrowser.Api.Images
|
||||
/// </summary>
|
||||
[Route("/Items/{Id}/Images/{Type}/{Index}/Index", "POST")]
|
||||
[Api(Description = "Updates the index for an item image")]
|
||||
[Authenticated]
|
||||
public class UpdateItemImageIndex : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
@ -137,6 +139,7 @@ namespace MediaBrowser.Api.Images
|
||||
[Route("/Items/{Id}/Images/{Type}", "DELETE")]
|
||||
[Route("/Items/{Id}/Images/{Type}/{Index}", "DELETE")]
|
||||
[Api(Description = "Deletes an item image")]
|
||||
[Authenticated]
|
||||
public class DeleteItemImage : DeleteImageRequest, IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
@ -153,6 +156,7 @@ namespace MediaBrowser.Api.Images
|
||||
[Route("/Users/{Id}/Images/{Type}", "DELETE")]
|
||||
[Route("/Users/{Id}/Images/{Type}/{Index}", "DELETE")]
|
||||
[Api(Description = "Deletes a user image")]
|
||||
[Authenticated]
|
||||
public class DeleteUserImage : DeleteImageRequest, IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
@ -169,6 +173,7 @@ namespace MediaBrowser.Api.Images
|
||||
[Route("/Users/{Id}/Images/{Type}", "POST")]
|
||||
[Route("/Users/{Id}/Images/{Type}/{Index}", "POST")]
|
||||
[Api(Description = "Posts a user image")]
|
||||
[Authenticated]
|
||||
public class PostUserImage : DeleteImageRequest, IRequiresRequestStream, IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
@ -191,6 +196,7 @@ namespace MediaBrowser.Api.Images
|
||||
[Route("/Items/{Id}/Images/{Type}", "POST")]
|
||||
[Route("/Items/{Id}/Images/{Type}/{Index}", "POST")]
|
||||
[Api(Description = "Posts an item image")]
|
||||
[Authenticated]
|
||||
public class PostItemImage : DeleteImageRequest, IRequiresRequestStream, IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
@ -359,45 +365,21 @@ namespace MediaBrowser.Api.Images
|
||||
_libraryManager.RootFolder :
|
||||
_libraryManager.GetItemById(request.Id);
|
||||
|
||||
if (!string.IsNullOrEmpty(request.Params))
|
||||
{
|
||||
ParseOptions(request, request.Params);
|
||||
}
|
||||
|
||||
return GetImage(request, item);
|
||||
return GetImage(request, item, false);
|
||||
}
|
||||
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
private void ParseOptions(ImageRequest request, string options)
|
||||
/// <summary>
|
||||
/// Gets the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
public object Head(GetItemImage request)
|
||||
{
|
||||
var vals = options.Split(';');
|
||||
var item = string.IsNullOrEmpty(request.Id) ?
|
||||
_libraryManager.RootFolder :
|
||||
_libraryManager.GetItemById(request.Id);
|
||||
|
||||
for (var i = 0; i < vals.Length; i++)
|
||||
{
|
||||
var val = vals[i];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
request.Tag = val;
|
||||
}
|
||||
else if (i == 1)
|
||||
{
|
||||
request.Format = (ImageOutputFormat)Enum.Parse(typeof(ImageOutputFormat), val, true);
|
||||
}
|
||||
else if (i == 2)
|
||||
{
|
||||
request.MaxWidth = int.Parse(val, _usCulture);
|
||||
}
|
||||
else if (i == 3)
|
||||
{
|
||||
request.MaxHeight = int.Parse(val, _usCulture);
|
||||
}
|
||||
}
|
||||
return GetImage(request, item, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -409,7 +391,7 @@ namespace MediaBrowser.Api.Images
|
||||
{
|
||||
var item = _userManager.Users.First(i => i.Id == request.Id);
|
||||
|
||||
return GetImage(request, item);
|
||||
return GetImage(request, item, false);
|
||||
}
|
||||
|
||||
public object Get(GetItemByNameImage request)
|
||||
@ -419,7 +401,7 @@ namespace MediaBrowser.Api.Images
|
||||
|
||||
var item = GetItemByName(request.Name, type, _libraryManager);
|
||||
|
||||
return GetImage(request, item);
|
||||
return GetImage(request, item, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -516,10 +498,10 @@ namespace MediaBrowser.Api.Images
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
/// <exception cref="ResourceNotFoundException">
|
||||
/// </exception>
|
||||
public object GetImage(ImageRequest request, IHasImages item)
|
||||
/// <exception cref="ResourceNotFoundException"></exception>
|
||||
public object GetImage(ImageRequest request, IHasImages item, bool isHeadRequest)
|
||||
{
|
||||
var imageInfo = GetImageInfo(request, item);
|
||||
|
||||
@ -528,9 +510,6 @@ namespace MediaBrowser.Api.Images
|
||||
throw new ResourceNotFoundException(string.Format("{0} does not have an image of type {1}", item.Name, request.Type));
|
||||
}
|
||||
|
||||
// See if we can avoid a file system lookup by looking for the file in ResolveArgs
|
||||
var originalFileImageDateModified = imageInfo.DateModified;
|
||||
|
||||
var supportedImageEnhancers = request.EnableImageEnhancers ? _imageProcessor.ImageEnhancers.Where(i =>
|
||||
{
|
||||
try
|
||||
@ -557,25 +536,68 @@ namespace MediaBrowser.Api.Images
|
||||
cacheDuration = TimeSpan.FromDays(365);
|
||||
}
|
||||
|
||||
// Avoid implicitly captured closure
|
||||
var currentItem = item;
|
||||
var currentRequest = request;
|
||||
|
||||
var responseHeaders = new Dictionary<string, string>
|
||||
{
|
||||
{"transferMode.dlna.org", "Interactive"},
|
||||
{"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}
|
||||
};
|
||||
|
||||
return ToCachedResult(cacheGuid, originalFileImageDateModified, cacheDuration, () => new ImageWriter
|
||||
{
|
||||
Item = currentItem,
|
||||
Request = currentRequest,
|
||||
Enhancers = supportedImageEnhancers,
|
||||
Image = imageInfo,
|
||||
ImageProcessor = _imageProcessor
|
||||
return GetImageResult(item,
|
||||
request,
|
||||
imageInfo,
|
||||
supportedImageEnhancers,
|
||||
contentType,
|
||||
cacheDuration,
|
||||
responseHeaders,
|
||||
isHeadRequest)
|
||||
.Result;
|
||||
}
|
||||
|
||||
}, contentType, responseHeaders);
|
||||
private async Task<object> GetImageResult(IHasImages item,
|
||||
ImageRequest request,
|
||||
ItemImageInfo image,
|
||||
List<IImageEnhancer> enhancers,
|
||||
string contentType,
|
||||
TimeSpan? cacheDuration,
|
||||
IDictionary<string, string> headers,
|
||||
bool isHeadRequest)
|
||||
{
|
||||
var cropwhitespace = request.Type == ImageType.Logo || request.Type == ImageType.Art;
|
||||
|
||||
if (request.CropWhitespace.HasValue)
|
||||
{
|
||||
cropwhitespace = request.CropWhitespace.Value;
|
||||
}
|
||||
|
||||
var options = new ImageProcessingOptions
|
||||
{
|
||||
CropWhiteSpace = cropwhitespace,
|
||||
Enhancers = enhancers,
|
||||
Height = request.Height,
|
||||
ImageIndex = request.Index ?? 0,
|
||||
Image = image,
|
||||
Item = item,
|
||||
MaxHeight = request.MaxHeight,
|
||||
MaxWidth = request.MaxWidth,
|
||||
Quality = request.Quality,
|
||||
Width = request.Width,
|
||||
OutputFormat = request.Format,
|
||||
AddPlayedIndicator = request.AddPlayedIndicator,
|
||||
PercentPlayed = request.PercentPlayed,
|
||||
UnplayedCount = request.UnplayedCount,
|
||||
BackgroundColor = request.BackgroundColor
|
||||
};
|
||||
|
||||
var file = await _imageProcessor.ProcessImage(options).ConfigureAwait(false);
|
||||
|
||||
return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
|
||||
{
|
||||
CacheDuration = cacheDuration,
|
||||
ResponseHeaders = headers,
|
||||
ContentType = contentType,
|
||||
IsHeadRequest = isHeadRequest,
|
||||
Path = file
|
||||
});
|
||||
}
|
||||
|
||||
private string GetMimeType(ImageOutputFormat format, string path)
|
||||
@ -596,6 +618,10 @@ namespace MediaBrowser.Api.Images
|
||||
{
|
||||
return Common.Net.MimeTypes.GetMimeType("i.png");
|
||||
}
|
||||
if (format == ImageOutputFormat.Webp)
|
||||
{
|
||||
return Common.Net.MimeTypes.GetMimeType("i.webp");
|
||||
}
|
||||
|
||||
return Common.Net.MimeTypes.GetMimeType(path);
|
||||
}
|
||||
|
@ -1,96 +0,0 @@
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using ServiceStack.Web;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Api.Images
|
||||
{
|
||||
/// <summary>
|
||||
/// Class ImageWriter
|
||||
/// </summary>
|
||||
public class ImageWriter : IStreamWriter, IHasOptions
|
||||
{
|
||||
public List<IImageEnhancer> Enhancers;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the request.
|
||||
/// </summary>
|
||||
/// <value>The request.</value>
|
||||
public ImageRequest Request { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the item.
|
||||
/// </summary>
|
||||
/// <value>The item.</value>
|
||||
public IHasImages Item { get; set; }
|
||||
/// <summary>
|
||||
/// The original image date modified
|
||||
/// </summary>
|
||||
public ItemImageInfo Image;
|
||||
|
||||
public IImageProcessor ImageProcessor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The _options
|
||||
/// </summary>
|
||||
private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
|
||||
/// <summary>
|
||||
/// Gets the options.
|
||||
/// </summary>
|
||||
/// <value>The options.</value>
|
||||
public IDictionary<string, string> Options
|
||||
{
|
||||
get { return _options; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes to.
|
||||
/// </summary>
|
||||
/// <param name="responseStream">The response stream.</param>
|
||||
public void WriteTo(Stream responseStream)
|
||||
{
|
||||
var task = WriteToAsync(responseStream);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes to async.
|
||||
/// </summary>
|
||||
/// <param name="responseStream">The response stream.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private Task WriteToAsync(Stream responseStream)
|
||||
{
|
||||
var cropwhitespace = Request.Type == ImageType.Logo || Request.Type == ImageType.Art;
|
||||
|
||||
if (Request.CropWhitespace.HasValue)
|
||||
{
|
||||
cropwhitespace = Request.CropWhitespace.Value;
|
||||
}
|
||||
|
||||
var options = new ImageProcessingOptions
|
||||
{
|
||||
CropWhiteSpace = cropwhitespace,
|
||||
Enhancers = Enhancers,
|
||||
Height = Request.Height,
|
||||
ImageIndex = Request.Index ?? 0,
|
||||
Image = Image,
|
||||
Item = Item,
|
||||
MaxHeight = Request.MaxHeight,
|
||||
MaxWidth = Request.MaxWidth,
|
||||
Quality = Request.Quality,
|
||||
Width = Request.Width,
|
||||
OutputFormat = Request.Format,
|
||||
AddPlayedIndicator = Request.AddPlayedIndicator,
|
||||
PercentPlayed = Request.PercentPlayed,
|
||||
UnplayedCount = Request.UnplayedCount,
|
||||
BackgroundColor = Request.BackgroundColor
|
||||
};
|
||||
|
||||
return ImageProcessor.ProcessImage(options, responseStream);
|
||||
}
|
||||
}
|
||||
}
|
@ -199,35 +199,33 @@ namespace MediaBrowser.Api.Images
|
||||
return _providerManager.GetRemoteImageProviderInfo(item).ToList();
|
||||
}
|
||||
|
||||
public object Get(GetRemoteImages request)
|
||||
public async Task<object> Get(GetRemoteImages request)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(request.Id);
|
||||
|
||||
var result = GetRemoteImageResult(item, request);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
return await GetRemoteImageResult(item, request).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public object Get(GetItemByNameRemoteImages request)
|
||||
public async Task<object> Get(GetItemByNameRemoteImages request)
|
||||
{
|
||||
var pathInfo = PathInfo.Parse(Request.PathInfo);
|
||||
var type = pathInfo.GetArgumentValue<string>(0);
|
||||
|
||||
var item = GetItemByName(request.Name, type, _libraryManager);
|
||||
|
||||
return GetRemoteImageResult(item, request);
|
||||
return await GetRemoteImageResult(item, request).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private RemoteImageResult GetRemoteImageResult(BaseItem item, BaseRemoteImageRequest request)
|
||||
private async Task<RemoteImageResult> GetRemoteImageResult(BaseItem item, BaseRemoteImageRequest request)
|
||||
{
|
||||
var images = _providerManager.GetAvailableRemoteImages(item, new RemoteImageQuery
|
||||
var images = await _providerManager.GetAvailableRemoteImages(item, new RemoteImageQuery
|
||||
{
|
||||
ProviderName = request.ProviderName,
|
||||
IncludeAllLanguages = request.IncludeAllLanguages,
|
||||
IncludeDisabledProviders = true,
|
||||
ImageType = request.Type
|
||||
|
||||
}, CancellationToken.None).Result;
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var imagesList = images.ToList();
|
||||
|
||||
@ -308,17 +306,10 @@ namespace MediaBrowser.Api.Images
|
||||
/// <returns>System.Object.</returns>
|
||||
public object Get(GetRemoteImage request)
|
||||
{
|
||||
var task = GetRemoteImage(request);
|
||||
|
||||
return task.Result;
|
||||
return GetAsync(request).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remote image.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>Task{System.Object}.</returns>
|
||||
private async Task<object> GetRemoteImage(GetRemoteImage request)
|
||||
public async Task<object> GetAsync(GetRemoteImage request)
|
||||
{
|
||||
var urlHash = request.ImageUrl.GetMD5();
|
||||
var pointerCachePath = GetFullCachePath(urlHash.ToString());
|
||||
|
@ -6,6 +6,7 @@ using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
@ -21,6 +22,7 @@ namespace MediaBrowser.Api
|
||||
{
|
||||
[Route("/Items/{Id}/ExternalIdInfos", "GET")]
|
||||
[Api(Description = "Gets external id infos for an item")]
|
||||
[Authenticated]
|
||||
public class GetExternalIdInfos : IReturn<List<ExternalIdInfo>>
|
||||
{
|
||||
/// <summary>
|
||||
@ -33,54 +35,63 @@ namespace MediaBrowser.Api
|
||||
|
||||
[Route("/Items/RemoteSearch/Movie", "POST")]
|
||||
[Api(Description = "Gets external id infos for an item")]
|
||||
[Authenticated]
|
||||
public class GetMovieRemoteSearchResults : RemoteSearchQuery<MovieInfo>, IReturn<List<RemoteSearchResult>>
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Items/RemoteSearch/Trailer", "POST")]
|
||||
[Api(Description = "Gets external id infos for an item")]
|
||||
[Authenticated]
|
||||
public class GetTrailerRemoteSearchResults : RemoteSearchQuery<TrailerInfo>, IReturn<List<RemoteSearchResult>>
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Items/RemoteSearch/AdultVideo", "POST")]
|
||||
[Api(Description = "Gets external id infos for an item")]
|
||||
[Authenticated]
|
||||
public class GetAdultVideoRemoteSearchResults : RemoteSearchQuery<ItemLookupInfo>, IReturn<List<RemoteSearchResult>>
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Items/RemoteSearch/Series", "POST")]
|
||||
[Api(Description = "Gets external id infos for an item")]
|
||||
[Authenticated]
|
||||
public class GetSeriesRemoteSearchResults : RemoteSearchQuery<SeriesInfo>, IReturn<List<RemoteSearchResult>>
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Items/RemoteSearch/Game", "POST")]
|
||||
[Api(Description = "Gets external id infos for an item")]
|
||||
[Authenticated]
|
||||
public class GetGameRemoteSearchResults : RemoteSearchQuery<GameInfo>, IReturn<List<RemoteSearchResult>>
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Items/RemoteSearch/BoxSet", "POST")]
|
||||
[Api(Description = "Gets external id infos for an item")]
|
||||
[Authenticated]
|
||||
public class GetBoxSetRemoteSearchResults : RemoteSearchQuery<BoxSetInfo>, IReturn<List<RemoteSearchResult>>
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Items/RemoteSearch/MusicArtist", "POST")]
|
||||
[Api(Description = "Gets external id infos for an item")]
|
||||
[Authenticated]
|
||||
public class GetMusicArtistRemoteSearchResults : RemoteSearchQuery<ArtistInfo>, IReturn<List<RemoteSearchResult>>
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Items/RemoteSearch/MusicAlbum", "POST")]
|
||||
[Api(Description = "Gets external id infos for an item")]
|
||||
[Authenticated]
|
||||
public class GetMusicAlbumRemoteSearchResults : RemoteSearchQuery<AlbumInfo>, IReturn<List<RemoteSearchResult>>
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Items/RemoteSearch/Person", "POST")]
|
||||
[Api(Description = "Gets external id infos for an item")]
|
||||
[Authenticated]
|
||||
public class GetPersonRemoteSearchResults : RemoteSearchQuery<PersonLookupInfo>, IReturn<List<RemoteSearchResult>>
|
||||
{
|
||||
}
|
||||
@ -98,6 +109,7 @@ namespace MediaBrowser.Api
|
||||
|
||||
[Route("/Items/RemoteSearch/Apply/{Id}", "POST")]
|
||||
[Api(Description = "Applies search criteria to an item and refreshes metadata")]
|
||||
[Authenticated]
|
||||
public class ApplySearchCriteria : RemoteSearchResult, IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "The item id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
@ -212,16 +224,23 @@ namespace MediaBrowser.Api
|
||||
}
|
||||
}
|
||||
|
||||
var task = item.RefreshMetadata(new MetadataRefreshOptions
|
||||
var service = new ItemRefreshService(_libraryManager)
|
||||
{
|
||||
Logger = Logger,
|
||||
Request = Request,
|
||||
ResultFactory = ResultFactory,
|
||||
SessionContext = SessionContext
|
||||
};
|
||||
|
||||
service.Post(new RefreshItem
|
||||
{
|
||||
Id = request.Id,
|
||||
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
|
||||
ImageRefreshMode = ImageRefreshMode.FullRefresh,
|
||||
ReplaceAllMetadata = true,
|
||||
ReplaceAllImages = true
|
||||
|
||||
}, CancellationToken.None);
|
||||
|
||||
Task.WaitAll(task);
|
||||
ReplaceAllImages = true,
|
||||
Recursive = true
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,6 +1,7 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
@ -12,10 +13,16 @@ namespace MediaBrowser.Api
|
||||
{
|
||||
public class BaseRefreshRequest : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Forced", Description = "Indicates if a normal or forced refresh should occur.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
|
||||
public bool Forced { get; set; }
|
||||
[ApiMember(Name = "MetadataRefreshMode", Description = "Specifies the metadata refresh mode", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
|
||||
public MetadataRefreshMode MetadataRefreshMode { get; set; }
|
||||
|
||||
[ApiMember(Name = "ReplaceAllImages", Description = "Determines if images should be replaced during the refresh.", IsRequired = true, DataType = "boolean", ParameterType = "query", Verb = "POST")]
|
||||
[ApiMember(Name = "ImageRefreshMode", Description = "Specifies the image refresh mode", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
|
||||
public ImageRefreshMode ImageRefreshMode { get; set; }
|
||||
|
||||
[ApiMember(Name = "ReplaceAllMetadata", Description = "Determines if metadata should be replaced. Only applicable if mode is FullRefresh", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
|
||||
public bool ReplaceAllMetadata { get; set; }
|
||||
|
||||
[ApiMember(Name = "ReplaceAllImages", Description = "Determines if images should be replaced. Only applicable if mode is FullRefresh", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
|
||||
public bool ReplaceAllImages { get; set; }
|
||||
}
|
||||
|
||||
@ -30,6 +37,7 @@ namespace MediaBrowser.Api
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class ItemRefreshService : BaseApiService
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
@ -146,10 +154,11 @@ namespace MediaBrowser.Api
|
||||
{
|
||||
return new MetadataRefreshOptions
|
||||
{
|
||||
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
|
||||
ImageRefreshMode = ImageRefreshMode.FullRefresh,
|
||||
ReplaceAllMetadata = request.Forced,
|
||||
ReplaceAllImages = request.ReplaceAllImages
|
||||
MetadataRefreshMode = request.MetadataRefreshMode,
|
||||
ImageRefreshMode = request.ImageRefreshMode,
|
||||
ReplaceAllImages = request.ReplaceAllImages,
|
||||
ReplaceAllMetadata = request.ReplaceAllMetadata,
|
||||
ForceSave = true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -19,6 +20,7 @@ namespace MediaBrowser.Api
|
||||
public string ItemId { get; set; }
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class ItemUpdateService : BaseApiService
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
@ -63,6 +65,11 @@ namespace MediaBrowser.Api
|
||||
}
|
||||
}
|
||||
|
||||
private DateTime NormalizeDateTime(DateTime val)
|
||||
{
|
||||
return DateTime.SpecifyKind(val, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
private void UpdateItem(BaseItemDto request, BaseItem item)
|
||||
{
|
||||
item.Name = request.Name;
|
||||
@ -108,6 +115,12 @@ namespace MediaBrowser.Api
|
||||
hasTags.Tags = request.Tags;
|
||||
}
|
||||
|
||||
var hasTaglines = item as IHasTaglines;
|
||||
if (hasTaglines != null)
|
||||
{
|
||||
hasTaglines.Taglines = request.Taglines;
|
||||
}
|
||||
|
||||
var hasShortOverview = item as IHasShortOverview;
|
||||
if (hasShortOverview != null)
|
||||
{
|
||||
@ -132,11 +145,11 @@ namespace MediaBrowser.Api
|
||||
|
||||
if (request.DateCreated.HasValue)
|
||||
{
|
||||
item.DateCreated = request.DateCreated.Value.ToUniversalTime();
|
||||
item.DateCreated = NormalizeDateTime(request.DateCreated.Value);
|
||||
}
|
||||
|
||||
item.EndDate = request.EndDate.HasValue ? request.EndDate.Value.ToUniversalTime() : (DateTime?)null;
|
||||
item.PremiereDate = request.PremiereDate.HasValue ? request.PremiereDate.Value.ToUniversalTime() : (DateTime?)null;
|
||||
item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : (DateTime?)null;
|
||||
item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : (DateTime?)null;
|
||||
item.ProductionYear = request.ProductionYear;
|
||||
item.OfficialRating = request.OfficialRating;
|
||||
item.CustomRating = request.CustomRating;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using MediaBrowser.Controller.Chapters;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using ServiceStack;
|
||||
using System.Linq;
|
||||
|
||||
@ -9,6 +10,7 @@ namespace MediaBrowser.Api.Library
|
||||
{
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class ChapterService : BaseApiService
|
||||
{
|
||||
private readonly IChapterManager _chapterManager;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using MediaBrowser.Controller.FileOrganization;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.FileOrganization;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using ServiceStack;
|
||||
@ -78,6 +79,7 @@ namespace MediaBrowser.Api.Library
|
||||
public bool RememberCorrection { get; set; }
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class FileOrganizationService : BaseApiService
|
||||
{
|
||||
private readonly IFileOrganizationService _iFileOrganizationService;
|
||||
|
@ -65,7 +65,7 @@ namespace MediaBrowser.Api.Library
|
||||
var rootFolderPath = appPaths.DefaultUserViewsPath;
|
||||
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
|
||||
|
||||
var shortcutFilename = Path.GetFileNameWithoutExtension(path);
|
||||
var shortcutFilename = fileSystem.GetFileNameWithoutExtension(path);
|
||||
|
||||
var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
||||
|
||||
|
@ -5,6 +5,7 @@ using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dto;
|
||||
@ -210,20 +211,23 @@ namespace MediaBrowser.Api.Library
|
||||
[Api(Description = "Gets all user media folders.")]
|
||||
public class GetMediaFolders : IReturn<ItemsResult>
|
||||
{
|
||||
|
||||
[ApiMember(Name = "IsHidden", Description = "Optional. Filter by folders that are marked hidden, or not.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
|
||||
public bool? IsHidden { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Library/Series/Added", "POST")]
|
||||
[Route("/Library/Series/Updated", "POST")]
|
||||
[Api(Description = "Reports that new episodes of a series have been added by an external source")]
|
||||
public class PostUpdatedSeries : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "TvdbId", Description = "Tvdb Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
[ApiMember(Name = "TvdbId", Description = "Tvdb Id", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string TvdbId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class LibraryService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class LibraryService : BaseApiService
|
||||
{
|
||||
/// <summary>
|
||||
@ -258,6 +262,13 @@ namespace MediaBrowser.Api.Library
|
||||
{
|
||||
var items = _libraryManager.GetUserRootFolder().Children.OrderBy(i => i.SortName).ToList();
|
||||
|
||||
if (request.IsHidden.HasValue)
|
||||
{
|
||||
var val = request.IsHidden.Value;
|
||||
|
||||
items = items.Where(i => i.IsHidden == val).ToList();
|
||||
}
|
||||
|
||||
// Get everything
|
||||
var fields = Enum.GetNames(typeof(ItemFields))
|
||||
.Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true))
|
||||
@ -273,6 +284,11 @@ namespace MediaBrowser.Api.Library
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public void Post(PostUpdatedSeries request)
|
||||
{
|
||||
Task.Run(() => _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None));
|
||||
}
|
||||
|
||||
public object Get(GetFile request)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(request.Id);
|
||||
@ -451,24 +467,12 @@ namespace MediaBrowser.Api.Library
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public void Delete(DeleteItem request)
|
||||
{
|
||||
var task = DeleteItem(request);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
private Task DeleteItem(DeleteItem request)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(request.Id);
|
||||
|
||||
var session = GetSession(_sessionManager);
|
||||
var task = _libraryManager.DeleteItem(item);
|
||||
|
||||
if (!session.UserId.HasValue || !_userManager.GetUserById(session.UserId.Value).Configuration.EnableContentDeletion)
|
||||
{
|
||||
throw new UnauthorizedAccessException("This operation requires a logged in user with delete access.");
|
||||
}
|
||||
|
||||
return _libraryManager.DeleteItem(item);
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,6 +1,7 @@
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using ServiceStack;
|
||||
@ -131,35 +132,10 @@ namespace MediaBrowser.Api.Library
|
||||
public bool RefreshLibrary { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Library/Downloaded", "POST")]
|
||||
public class ReportContentDownloaded : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Path", Description = "The path being downloaded to.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string Path { get; set; }
|
||||
|
||||
[ApiMember(Name = "ImageUrl", Description = "Optional thumbnail image url of the content.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string ImageUrl { get; set; }
|
||||
|
||||
[ApiMember(Name = "Name", Description = "The name of the content.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Library/Downloading", "POST")]
|
||||
public class ReportContentDownloading : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Path", Description = "The path being downloaded to.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string Path { get; set; }
|
||||
|
||||
[ApiMember(Name = "ImageUrl", Description = "Optional thumbnail image url of the content.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string ImageUrl { get; set; }
|
||||
|
||||
[ApiMember(Name = "Name", Description = "The name of the content.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class LibraryStructureService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class LibraryStructureService : BaseApiService
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -1,5 +1,6 @@
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
@ -267,6 +268,7 @@ namespace MediaBrowser.Api.LiveTv
|
||||
public string UserId { get; set; }
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class LiveTvService : BaseApiService
|
||||
{
|
||||
private readonly ILiveTvManager _liveTvManager;
|
||||
@ -280,7 +282,7 @@ namespace MediaBrowser.Api.LiveTv
|
||||
|
||||
private void AssertUserCanManageLiveTv()
|
||||
{
|
||||
var user = AuthorizationRequestFilterAttribute.GetCurrentUser(Request, _userManager);
|
||||
var user = SessionContext.GetUser(Request);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
@ -293,16 +295,16 @@ namespace MediaBrowser.Api.LiveTv
|
||||
}
|
||||
}
|
||||
|
||||
public object Get(GetLiveTvInfo request)
|
||||
public async Task<object> Get(GetLiveTvInfo request)
|
||||
{
|
||||
var info = _liveTvManager.GetLiveTvInfo(CancellationToken.None).Result;
|
||||
var info = await _liveTvManager.GetLiveTvInfo(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(info);
|
||||
}
|
||||
|
||||
public object Get(GetChannels request)
|
||||
public async Task<object> Get(GetChannels request)
|
||||
{
|
||||
var result = _liveTvManager.GetChannels(new LiveTvChannelQuery
|
||||
var result = await _liveTvManager.GetChannels(new LiveTvChannelQuery
|
||||
{
|
||||
ChannelType = request.Type,
|
||||
UserId = request.UserId,
|
||||
@ -312,26 +314,26 @@ namespace MediaBrowser.Api.LiveTv
|
||||
IsLiked = request.IsLiked,
|
||||
IsDisliked = request.IsDisliked
|
||||
|
||||
}, CancellationToken.None).Result;
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
}
|
||||
|
||||
public object Get(GetChannel request)
|
||||
public async Task<object> Get(GetChannel request)
|
||||
{
|
||||
var user = string.IsNullOrEmpty(request.UserId) ? null : _userManager.GetUserById(new Guid(request.UserId));
|
||||
|
||||
var result = _liveTvManager.GetChannel(request.Id, CancellationToken.None, user).Result;
|
||||
var result = await _liveTvManager.GetChannel(request.Id, CancellationToken.None, user).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
}
|
||||
|
||||
public object Get(GetLiveTvFolder request)
|
||||
public async Task<object> Get(GetLiveTvFolder request)
|
||||
{
|
||||
return ToOptimizedResult(_liveTvManager.GetLiveTvFolder(request.UserId, CancellationToken.None).Result);
|
||||
return ToOptimizedResult(await _liveTvManager.GetLiveTvFolder(request.UserId, CancellationToken.None).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
public object Get(GetPrograms request)
|
||||
public async Task<object> Get(GetPrograms request)
|
||||
{
|
||||
var query = new ProgramQuery
|
||||
{
|
||||
@ -359,12 +361,12 @@ namespace MediaBrowser.Api.LiveTv
|
||||
query.MaxEndDate = DateTime.Parse(request.MaxEndDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
|
||||
}
|
||||
|
||||
var result = _liveTvManager.GetPrograms(query, CancellationToken.None).Result;
|
||||
var result = await _liveTvManager.GetPrograms(query, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
}
|
||||
|
||||
public object Get(GetRecommendedPrograms request)
|
||||
public async Task<object> Get(GetRecommendedPrograms request)
|
||||
{
|
||||
var query = new RecommendedProgramQuery
|
||||
{
|
||||
@ -374,7 +376,7 @@ namespace MediaBrowser.Api.LiveTv
|
||||
HasAired = request.HasAired
|
||||
};
|
||||
|
||||
var result = _liveTvManager.GetRecommendedPrograms(query, CancellationToken.None).Result;
|
||||
var result = await _liveTvManager.GetRecommendedPrograms(query, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
}
|
||||
@ -384,9 +386,9 @@ namespace MediaBrowser.Api.LiveTv
|
||||
return Get(request);
|
||||
}
|
||||
|
||||
public object Get(GetRecordings request)
|
||||
public async Task<object> Get(GetRecordings request)
|
||||
{
|
||||
var result = _liveTvManager.GetRecordings(new RecordingQuery
|
||||
var result = await _liveTvManager.GetRecordings(new RecordingQuery
|
||||
{
|
||||
ChannelId = request.ChannelId,
|
||||
UserId = request.UserId,
|
||||
@ -397,35 +399,35 @@ namespace MediaBrowser.Api.LiveTv
|
||||
SeriesTimerId = request.SeriesTimerId,
|
||||
IsInProgress = request.IsInProgress
|
||||
|
||||
}, CancellationToken.None).Result;
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
}
|
||||
|
||||
public object Get(GetRecording request)
|
||||
public async Task<object> Get(GetRecording request)
|
||||
{
|
||||
var user = string.IsNullOrEmpty(request.UserId) ? null : _userManager.GetUserById(new Guid(request.UserId));
|
||||
|
||||
var result = _liveTvManager.GetRecording(request.Id, CancellationToken.None, user).Result;
|
||||
var result = await _liveTvManager.GetRecording(request.Id, CancellationToken.None, user).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
}
|
||||
|
||||
public object Get(GetTimer request)
|
||||
public async Task<object> Get(GetTimer request)
|
||||
{
|
||||
var result = _liveTvManager.GetTimer(request.Id, CancellationToken.None).Result;
|
||||
var result = await _liveTvManager.GetTimer(request.Id, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
}
|
||||
|
||||
public object Get(GetTimers request)
|
||||
public async Task<object> Get(GetTimers request)
|
||||
{
|
||||
var result = _liveTvManager.GetTimers(new TimerQuery
|
||||
var result = await _liveTvManager.GetTimers(new TimerQuery
|
||||
{
|
||||
ChannelId = request.ChannelId,
|
||||
SeriesTimerId = request.SeriesTimerId
|
||||
|
||||
}, CancellationToken.None).Result;
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
}
|
||||
@ -457,21 +459,21 @@ namespace MediaBrowser.Api.LiveTv
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
public object Get(GetSeriesTimers request)
|
||||
public async Task<object> Get(GetSeriesTimers request)
|
||||
{
|
||||
var result = _liveTvManager.GetSeriesTimers(new SeriesTimerQuery
|
||||
var result = await _liveTvManager.GetSeriesTimers(new SeriesTimerQuery
|
||||
{
|
||||
SortOrder = request.SortOrder,
|
||||
SortBy = request.SortBy
|
||||
|
||||
}, CancellationToken.None).Result;
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
}
|
||||
|
||||
public object Get(GetSeriesTimer request)
|
||||
public async Task<object> Get(GetSeriesTimer request)
|
||||
{
|
||||
var result = _liveTvManager.GetSeriesTimer(request.Id, CancellationToken.None).Result;
|
||||
var result = await _liveTvManager.GetSeriesTimer(request.Id, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
}
|
||||
@ -494,27 +496,27 @@ namespace MediaBrowser.Api.LiveTv
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
public object Get(GetDefaultTimer request)
|
||||
public async Task<object> Get(GetDefaultTimer request)
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.ProgramId))
|
||||
{
|
||||
var result = _liveTvManager.GetNewTimerDefaults(CancellationToken.None).Result;
|
||||
var result = await _liveTvManager.GetNewTimerDefaults(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = _liveTvManager.GetNewTimerDefaults(request.ProgramId, CancellationToken.None).Result;
|
||||
var result = await _liveTvManager.GetNewTimerDefaults(request.ProgramId, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
}
|
||||
}
|
||||
|
||||
public object Get(GetProgram request)
|
||||
public async Task<object> Get(GetProgram request)
|
||||
{
|
||||
var user = string.IsNullOrEmpty(request.UserId) ? null : _userManager.GetUserById(new Guid(request.UserId));
|
||||
|
||||
var result = _liveTvManager.GetProgram(request.Id, CancellationToken.None, user).Result;
|
||||
var result = await _liveTvManager.GetProgram(request.Id, CancellationToken.None, user).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
}
|
||||
@ -537,23 +539,23 @@ namespace MediaBrowser.Api.LiveTv
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
public object Get(GetRecordingGroups request)
|
||||
public async Task<object> Get(GetRecordingGroups request)
|
||||
{
|
||||
var result = _liveTvManager.GetRecordingGroups(new RecordingGroupQuery
|
||||
var result = await _liveTvManager.GetRecordingGroups(new RecordingGroupQuery
|
||||
{
|
||||
UserId = request.UserId
|
||||
|
||||
}, CancellationToken.None).Result;
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
}
|
||||
|
||||
public object Get(GetRecordingGroup request)
|
||||
public async Task<object> Get(GetRecordingGroup request)
|
||||
{
|
||||
var result = _liveTvManager.GetRecordingGroups(new RecordingGroupQuery
|
||||
var result = await _liveTvManager.GetRecordingGroups(new RecordingGroupQuery
|
||||
{
|
||||
|
||||
}, CancellationToken.None).Result;
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var group = result.Items.FirstOrDefault(i => string.Equals(i.Id, request.Id, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using MediaBrowser.Controller.Localization;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using ServiceStack;
|
||||
@ -42,6 +43,7 @@ namespace MediaBrowser.Api
|
||||
/// <summary>
|
||||
/// Class CulturesService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class LocalizationService : BaseApiService
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -49,6 +49,9 @@
|
||||
<RunPostBuildEvent>Always</RunPostBuildEvent>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="MoreLinq">
|
||||
<HintPath>..\packages\morelinq.1.0.16006\lib\net35\MoreLinq.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
@ -65,27 +68,25 @@
|
||||
<Compile Include="..\SharedVersion.cs">
|
||||
<Link>Properties\SharedVersion.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="BrandingService.cs" />
|
||||
<Compile Include="ChannelService.cs" />
|
||||
<Compile Include="Dlna\DlnaServerService.cs" />
|
||||
<Compile Include="Dlna\DlnaService.cs" />
|
||||
<Compile Include="Library\ChapterService.cs" />
|
||||
<Compile Include="Library\SubtitleService.cs" />
|
||||
<Compile Include="PlaylistService.cs" />
|
||||
<Compile Include="Subtitles\SubtitleService.cs" />
|
||||
<Compile Include="Movies\CollectionService.cs" />
|
||||
<Compile Include="Music\AlbumsService.cs" />
|
||||
<Compile Include="AppThemeService.cs" />
|
||||
<Compile Include="BaseApiService.cs" />
|
||||
<Compile Include="ConfigurationService.cs" />
|
||||
<Compile Include="DefaultTheme\DefaultThemeService.cs" />
|
||||
<Compile Include="DefaultTheme\Models.cs" />
|
||||
<Compile Include="DisplayPreferencesService.cs" />
|
||||
<Compile Include="EnvironmentService.cs" />
|
||||
<Compile Include="AuthorizationRequestFilterAttribute.cs" />
|
||||
<Compile Include="GamesService.cs" />
|
||||
<Compile Include="IHasItemFields.cs" />
|
||||
<Compile Include="Images\ImageByNameService.cs" />
|
||||
<Compile Include="Images\ImageRequest.cs" />
|
||||
<Compile Include="Images\ImageService.cs" />
|
||||
<Compile Include="Images\ImageWriter.cs" />
|
||||
<Compile Include="Music\InstantMixService.cs" />
|
||||
<Compile Include="ItemLookupService.cs" />
|
||||
<Compile Include="ItemRefreshService.cs" />
|
||||
@ -101,7 +102,7 @@
|
||||
<Compile Include="NotificationsService.cs" />
|
||||
<Compile Include="PackageReviewService.cs" />
|
||||
<Compile Include="PackageService.cs" />
|
||||
<Compile Include="Playback\EndlessStreamCopy.cs" />
|
||||
<Compile Include="Playback\BifService.cs" />
|
||||
<Compile Include="Playback\Hls\BaseHlsService.cs" />
|
||||
<Compile Include="Playback\Hls\DynamicHlsService.cs" />
|
||||
<Compile Include="Playback\Hls\HlsSegmentService.cs" />
|
||||
@ -120,9 +121,12 @@
|
||||
<Compile Include="ScheduledTasks\ScheduledTasksWebSocketListener.cs" />
|
||||
<Compile Include="ApiEntryPoint.cs" />
|
||||
<Compile Include="SearchService.cs" />
|
||||
<Compile Include="SessionsService.cs" />
|
||||
<Compile Include="Session\SessionsService.cs" />
|
||||
<Compile Include="SimilarItemsHelper.cs" />
|
||||
<Compile Include="SystemService.cs" />
|
||||
<Compile Include="Sync\SyncService.cs" />
|
||||
<Compile Include="System\ActivityLogService.cs" />
|
||||
<Compile Include="System\ActivityLogWebSocketListener.cs" />
|
||||
<Compile Include="System\SystemService.cs" />
|
||||
<Compile Include="Movies\TrailersService.cs" />
|
||||
<Compile Include="TvShowsService.cs" />
|
||||
<Compile Include="UserLibrary\ArtistsService.cs" />
|
||||
@ -133,15 +137,15 @@
|
||||
<Compile Include="UserLibrary\ItemsService.cs" />
|
||||
<Compile Include="UserLibrary\MusicGenresService.cs" />
|
||||
<Compile Include="UserLibrary\PersonsService.cs" />
|
||||
<Compile Include="UserLibrary\PlaystateService.cs" />
|
||||
<Compile Include="UserLibrary\StudiosService.cs" />
|
||||
<Compile Include="UserLibrary\UserLibraryService.cs" />
|
||||
<Compile Include="UserLibrary\YearsService.cs" />
|
||||
<Compile Include="UserService.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="VideosService.cs" />
|
||||
<Compile Include="WebSocket\LogFileWebSocketListener.cs" />
|
||||
<Compile Include="WebSocket\SessionInfoWebSocketListener.cs" />
|
||||
<Compile Include="WebSocket\SystemInfoWebSocketListener.cs" />
|
||||
<Compile Include="Session\SessionInfoWebSocketListener.cs" />
|
||||
<Compile Include="System\SystemInfoWebSocketListener.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
|
||||
@ -160,13 +164,12 @@
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<PropertyGroup>
|
||||
<PostBuildEvent>
|
||||
</PostBuildEvent>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(SolutionDir)\.nuget\nuget.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " />
|
||||
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
|
@ -1,5 +1,7 @@
|
||||
using MediaBrowser.Controller.Collections;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Collections;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
@ -45,6 +47,7 @@ namespace MediaBrowser.Api.Movies
|
||||
public Guid Id { get; set; }
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class CollectionService : BaseApiService
|
||||
{
|
||||
private readonly ICollectionManager _collectionManager;
|
||||
@ -56,17 +59,16 @@ namespace MediaBrowser.Api.Movies
|
||||
_dtoService = dtoService;
|
||||
}
|
||||
|
||||
public object Post(CreateCollection request)
|
||||
public async Task<object> Post(CreateCollection request)
|
||||
{
|
||||
var task = _collectionManager.CreateCollection(new CollectionCreationOptions
|
||||
var item = await _collectionManager.CreateCollection(new CollectionCreationOptions
|
||||
{
|
||||
IsLocked = request.IsLocked,
|
||||
Name = request.Name,
|
||||
ParentId = request.ParentId,
|
||||
ItemIdList = (request.Ids ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => new Guid(i)).ToList()
|
||||
});
|
||||
|
||||
var item = task.Result;
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
var dto = _dtoService.GetBaseItemDto(item, new List<ItemFields>());
|
||||
|
||||
@ -90,9 +92,4 @@ namespace MediaBrowser.Api.Movies
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
}
|
||||
|
||||
public class CollectionCreationResult
|
||||
{
|
||||
public string Id { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
@ -64,6 +65,7 @@ namespace MediaBrowser.Api.Movies
|
||||
/// <summary>
|
||||
/// Class MoviesService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class MoviesService : BaseApiService
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -2,6 +2,7 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using ServiceStack;
|
||||
|
||||
@ -18,6 +19,7 @@ namespace MediaBrowser.Api.Movies
|
||||
/// <summary>
|
||||
/// Class TrailersService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class TrailersService : BaseApiService
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -2,10 +2,10 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Api.Music
|
||||
@ -15,6 +15,7 @@ namespace MediaBrowser.Api.Music
|
||||
{
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class AlbumsService : BaseApiService
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -2,6 +2,7 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using ServiceStack;
|
||||
using System.Collections.Generic;
|
||||
@ -33,6 +34,21 @@ namespace MediaBrowser.Api.Music
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Artists/InstantMix", "GET", Summary = "Creates an instant playlist based on a given artist")]
|
||||
public class GetInstantMixFromArtistId : BaseGetSimilarItems
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "The artist Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
[Route("/MusicGenres/InstantMix", "GET", Summary = "Creates an instant playlist based on a music genre")]
|
||||
public class GetInstantMixFromMusicGenreId : BaseGetSimilarItems
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "The genre Id", IsRequired = true, DataType = "string", ParameterType = "querypath", Verb = "GET")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class InstantMixService : BaseApiService
|
||||
{
|
||||
private readonly IUserManager _userManager;
|
||||
@ -49,6 +65,28 @@ namespace MediaBrowser.Api.Music
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
public object Get(GetInstantMixFromArtistId request)
|
||||
{
|
||||
var item = (MusicArtist)_libraryManager.GetItemById(request.Id);
|
||||
|
||||
var user = _userManager.GetUserById(request.UserId.Value);
|
||||
|
||||
var items = _musicManager.GetInstantMixFromArtist(item.Name, user);
|
||||
|
||||
return GetResult(items, user, request);
|
||||
}
|
||||
|
||||
public object Get(GetInstantMixFromMusicGenreId request)
|
||||
{
|
||||
var item = (MusicGenre)_libraryManager.GetItemById(request.Id);
|
||||
|
||||
var user = _userManager.GetUserById(request.UserId.Value);
|
||||
|
||||
var items = _musicManager.GetInstantMixFromGenres(new[] { item.Name }, user);
|
||||
|
||||
return GetResult(items, user, request);
|
||||
}
|
||||
|
||||
public object Get(GetInstantMixFromSong request)
|
||||
{
|
||||
var item = (Audio)_libraryManager.GetItemById(request.Id);
|
||||
|
@ -1,4 +1,5 @@
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Notifications;
|
||||
using MediaBrowser.Model.Notifications;
|
||||
using ServiceStack;
|
||||
@ -82,6 +83,7 @@ namespace MediaBrowser.Api
|
||||
public string Ids { get; set; }
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class NotificationsService : BaseApiService
|
||||
{
|
||||
private readonly INotificationsRepository _notificationsRepo;
|
||||
|
@ -1,5 +1,6 @@
|
||||
using MediaBrowser.Common.Constants;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using ServiceStack;
|
||||
@ -96,6 +97,7 @@ namespace MediaBrowser.Api
|
||||
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class PackageReviewService : BaseApiService
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
@ -1,6 +1,7 @@
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Updates;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Updates;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
@ -121,6 +122,7 @@ namespace MediaBrowser.Api
|
||||
/// <summary>
|
||||
/// Class PackageService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class PackageService : BaseApiService
|
||||
{
|
||||
private readonly IInstallationManager _installationManager;
|
||||
|
@ -13,7 +13,6 @@ using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Library;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using System;
|
||||
@ -123,7 +122,11 @@ namespace MediaBrowser.Api.Playback
|
||||
|
||||
var outputFileExtension = GetOutputFileExtension(state);
|
||||
|
||||
return Path.Combine(folder, GetCommandLineArguments("dummy\\dummy", state, false).GetMD5() + (outputFileExtension ?? string.Empty).ToLower());
|
||||
var data = GetCommandLineArguments("dummy\\dummy", state, false);
|
||||
|
||||
data += "-" + (state.Request.DeviceId ?? string.Empty);
|
||||
|
||||
return Path.Combine(folder, data.GetMD5().ToString("N") + (outputFileExtension ?? string.Empty).ToLower());
|
||||
}
|
||||
|
||||
protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
@ -138,14 +141,9 @@ namespace MediaBrowser.Api.Playback
|
||||
{
|
||||
var time = request.StartTimeTicks;
|
||||
|
||||
if (time.HasValue)
|
||||
if (time.HasValue && time.Value > 0)
|
||||
{
|
||||
var seconds = TimeSpan.FromTicks(time.Value).TotalSeconds;
|
||||
|
||||
if (seconds > 0)
|
||||
{
|
||||
return string.Format("-ss {0}", seconds.ToString(UsCulture));
|
||||
}
|
||||
return string.Format("-ss {0}", MediaEncoder.GetTimeParameter(time.Value));
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
@ -319,7 +317,7 @@ namespace MediaBrowser.Api.Playback
|
||||
switch (qualitySetting)
|
||||
{
|
||||
case EncodingQuality.HighSpeed:
|
||||
param = "-preset ultrafast";
|
||||
param = "-preset superfast";
|
||||
break;
|
||||
case EncodingQuality.HighQuality:
|
||||
param = "-preset superfast";
|
||||
@ -350,16 +348,16 @@ namespace MediaBrowser.Api.Playback
|
||||
var profileScore = 0;
|
||||
|
||||
string crf;
|
||||
var qmin = "0";
|
||||
var qmax = "50";
|
||||
|
||||
switch (qualitySetting)
|
||||
{
|
||||
case EncodingQuality.HighSpeed:
|
||||
crf = "12";
|
||||
profileScore = 2;
|
||||
crf = "10";
|
||||
break;
|
||||
case EncodingQuality.HighQuality:
|
||||
crf = "8";
|
||||
profileScore = 1;
|
||||
crf = "6";
|
||||
break;
|
||||
case EncodingQuality.MaxQuality:
|
||||
crf = "4";
|
||||
@ -371,14 +369,17 @@ namespace MediaBrowser.Api.Playback
|
||||
if (isVc1)
|
||||
{
|
||||
profileScore++;
|
||||
// Max of 2
|
||||
profileScore = Math.Min(profileScore, 2);
|
||||
}
|
||||
|
||||
// Max of 2
|
||||
profileScore = Math.Min(profileScore, 2);
|
||||
|
||||
// http://www.webmproject.org/docs/encoder-parameters/
|
||||
param = string.Format("-speed 16 -quality good -profile:v {0} -slices 8 -crf {1}",
|
||||
param = string.Format("-speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
|
||||
profileScore.ToString(UsCulture),
|
||||
crf);
|
||||
crf,
|
||||
qmin,
|
||||
qmax);
|
||||
}
|
||||
|
||||
else if (string.Equals(videoCodec, "mpeg4", StringComparison.OrdinalIgnoreCase))
|
||||
@ -469,11 +470,11 @@ namespace MediaBrowser.Api.Playback
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="outputVideoCodec">The output video codec.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="allowTimeStampCopy">if set to <c>true</c> [allow time stamp copy].</param>
|
||||
/// <returns>System.String.</returns>
|
||||
protected string GetOutputSizeParam(StreamState state,
|
||||
string outputVideoCodec,
|
||||
CancellationToken cancellationToken)
|
||||
bool allowTimeStampCopy = true)
|
||||
{
|
||||
// http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
|
||||
|
||||
@ -562,11 +563,14 @@ namespace MediaBrowser.Api.Playback
|
||||
|
||||
if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream)
|
||||
{
|
||||
var subParam = GetTextSubtitleParam(state, cancellationToken);
|
||||
var subParam = GetTextSubtitleParam(state);
|
||||
|
||||
filters.Add(subParam);
|
||||
|
||||
output += " -copyts";
|
||||
if (allowTimeStampCopy)
|
||||
{
|
||||
output += " -copyts";
|
||||
}
|
||||
}
|
||||
|
||||
if (filters.Count > 0)
|
||||
@ -581,12 +585,10 @@ namespace MediaBrowser.Api.Playback
|
||||
/// Gets the text subtitle param.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
protected string GetTextSubtitleParam(StreamState state,
|
||||
CancellationToken cancellationToken)
|
||||
protected string GetTextSubtitleParam(StreamState state)
|
||||
{
|
||||
var seconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds;
|
||||
var seconds = Math.Round(TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds);
|
||||
|
||||
if (state.SubtitleStream.IsExternal)
|
||||
{
|
||||
@ -604,17 +606,17 @@ namespace MediaBrowser.Api.Playback
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Perhaps also use original_size=1920x800
|
||||
// TODO: Perhaps also use original_size=1920x800 ??
|
||||
return string.Format("subtitles=filename='{0}'{1},setpts=PTS -{2}/TB",
|
||||
subtitlePath.Replace('\\', '/').Replace(":/", "\\:/"),
|
||||
charsetParam,
|
||||
Math.Round(seconds).ToString(UsCulture));
|
||||
seconds.ToString(UsCulture));
|
||||
}
|
||||
|
||||
return string.Format("subtitles='{0}:si={1}',setpts=PTS -{2}/TB",
|
||||
state.MediaPath.Replace('\\', '/').Replace(":/", "\\:/"),
|
||||
state.InternalSubtitleStreamOffset.ToString(UsCulture),
|
||||
Math.Round(seconds).ToString(UsCulture));
|
||||
seconds.ToString(UsCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -623,7 +625,7 @@ namespace MediaBrowser.Api.Playback
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="outputVideoCodec">The output video codec.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
protected string GetInternalGraphicalSubtitleParam(StreamState state, string outputVideoCodec)
|
||||
protected string GetGraphicalSubtitleParam(StreamState state, string outputVideoCodec)
|
||||
{
|
||||
var outputSizeParam = string.Empty;
|
||||
|
||||
@ -632,7 +634,7 @@ namespace MediaBrowser.Api.Playback
|
||||
// Add resolution params, if specified
|
||||
if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue)
|
||||
{
|
||||
outputSizeParam = GetOutputSizeParam(state, outputVideoCodec, CancellationToken.None).TrimEnd('"');
|
||||
outputSizeParam = GetOutputSizeParam(state, outputVideoCodec).TrimEnd('"');
|
||||
outputSizeParam = "," + outputSizeParam.Substring(outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
@ -772,6 +774,11 @@ namespace MediaBrowser.Api.Playback
|
||||
return "copy";
|
||||
}
|
||||
|
||||
protected virtual bool SupportsThrottling
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the input argument.
|
||||
/// </summary>
|
||||
@ -779,6 +786,19 @@ namespace MediaBrowser.Api.Playback
|
||||
/// <returns>System.String.</returns>
|
||||
protected string GetInputArgument(StreamState state)
|
||||
{
|
||||
if (state.InputProtocol == MediaProtocol.File &&
|
||||
state.RunTimeTicks.HasValue &&
|
||||
state.VideoType == VideoType.VideoFile &&
|
||||
!string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo)
|
||||
{
|
||||
var url = "http://localhost:" + ServerConfigurationManager.Configuration.HttpServerPortNumber.ToString(UsCulture) + "/mediabrowser/videos/" + state.Request.Id + "/stream?static=true&Throttle=true&mediaSourceId=" + state.Request.MediaSourceId;
|
||||
|
||||
return string.Format("\"{0}\"", url);
|
||||
}
|
||||
}
|
||||
|
||||
var protocol = state.InputProtocol;
|
||||
|
||||
var inputPath = new[] { state.MediaPath };
|
||||
@ -794,6 +814,81 @@ namespace MediaBrowser.Api.Playback
|
||||
return MediaEncoder.GetInputArgument(inputPath, protocol);
|
||||
}
|
||||
|
||||
private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
|
||||
{
|
||||
if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath))
|
||||
{
|
||||
state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(state.MediaPath))
|
||||
{
|
||||
var checkCodecs = false;
|
||||
|
||||
if (string.Equals(state.ItemType, typeof(LiveTvChannel).Name))
|
||||
{
|
||||
var streamInfo = await LiveTvManager.GetChannelStream(state.Request.Id, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
state.LiveTvStreamId = streamInfo.Id;
|
||||
|
||||
if (!string.IsNullOrEmpty(streamInfo.Path))
|
||||
{
|
||||
state.MediaPath = streamInfo.Path;
|
||||
state.InputProtocol = MediaProtocol.File;
|
||||
|
||||
await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(streamInfo.Url))
|
||||
{
|
||||
state.MediaPath = streamInfo.Url;
|
||||
state.InputProtocol = MediaProtocol.Http;
|
||||
}
|
||||
|
||||
AttachMediaStreamInfo(state, streamInfo.MediaStreams, state.VideoRequest, state.RequestedUrl);
|
||||
checkCodecs = true;
|
||||
}
|
||||
|
||||
else if (string.Equals(state.ItemType, typeof(LiveTvVideoRecording).Name) ||
|
||||
string.Equals(state.ItemType, typeof(LiveTvAudioRecording).Name))
|
||||
{
|
||||
var streamInfo = await LiveTvManager.GetRecordingStream(state.Request.Id, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
state.LiveTvStreamId = streamInfo.Id;
|
||||
|
||||
if (!string.IsNullOrEmpty(streamInfo.Path))
|
||||
{
|
||||
state.MediaPath = streamInfo.Path;
|
||||
state.InputProtocol = MediaProtocol.File;
|
||||
|
||||
await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(streamInfo.Url))
|
||||
{
|
||||
state.MediaPath = streamInfo.Url;
|
||||
state.InputProtocol = MediaProtocol.Http;
|
||||
}
|
||||
|
||||
AttachMediaStreamInfo(state, streamInfo.MediaStreams, state.VideoRequest, state.RequestedUrl);
|
||||
checkCodecs = true;
|
||||
}
|
||||
|
||||
var videoRequest = state.VideoRequest;
|
||||
|
||||
if (videoRequest != null && checkCodecs)
|
||||
{
|
||||
if (state.VideoStream != null && CanStreamCopyVideo(videoRequest, state.VideoStream))
|
||||
{
|
||||
state.OutputVideoCodec = "copy";
|
||||
}
|
||||
|
||||
if (state.AudioStream != null && CanStreamCopyAudio(videoRequest, state.AudioStream, state.SupportedAudioCodecs))
|
||||
{
|
||||
state.OutputAudioCodec = "copy";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the FFMPEG.
|
||||
/// </summary>
|
||||
@ -811,10 +906,7 @@ namespace MediaBrowser.Api.Playback
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
|
||||
|
||||
if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath))
|
||||
{
|
||||
state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
|
||||
|
||||
var commandLineArgs = GetCommandLineArguments(outputPath, state, true);
|
||||
|
||||
@ -849,7 +941,6 @@ namespace MediaBrowser.Api.Playback
|
||||
ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath,
|
||||
TranscodingJobType,
|
||||
process,
|
||||
state.Request.StartTimeTicks,
|
||||
state.Request.DeviceId,
|
||||
state,
|
||||
cancellationTokenSource);
|
||||
@ -866,7 +957,7 @@ namespace MediaBrowser.Api.Playback
|
||||
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(commandLineLogMessage + Environment.NewLine + Environment.NewLine);
|
||||
await state.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
process.Exited += (sender, args) => OnFfMpegProcessExited(process, state);
|
||||
process.Exited += (sender, args) => OnFfMpegProcessExited(process, state, outputPath);
|
||||
|
||||
try
|
||||
{
|
||||
@ -892,18 +983,6 @@ namespace MediaBrowser.Api.Playback
|
||||
{
|
||||
await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Allow a small amount of time to buffer a little
|
||||
if (state.IsInputVideo)
|
||||
{
|
||||
await Task.Delay(500, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// This is arbitrary, but add a little buffer time when internet streaming
|
||||
if (state.InputProtocol != MediaProtocol.File)
|
||||
{
|
||||
await Task.Delay(3000, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async void StartStreamingLog(StreamState state, Stream source, Stream target)
|
||||
@ -1061,7 +1140,8 @@ namespace MediaBrowser.Api.Playback
|
||||
// Make sure we don't request a bitrate higher than the source
|
||||
var currentBitrate = audioStream == null ? request.AudioBitRate.Value : audioStream.BitRate ?? request.AudioBitRate.Value;
|
||||
|
||||
return Math.Min(currentBitrate, request.AudioBitRate.Value);
|
||||
return request.AudioBitRate.Value;
|
||||
//return Math.Min(currentBitrate, request.AudioBitRate.Value);
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -1091,8 +1171,16 @@ namespace MediaBrowser.Api.Playback
|
||||
/// </summary>
|
||||
/// <param name="process">The process.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
private void OnFfMpegProcessExited(Process process, StreamState state)
|
||||
/// <param name="outputPath">The output path.</param>
|
||||
private void OnFfMpegProcessExited(Process process, StreamState state, string outputPath)
|
||||
{
|
||||
var job = ApiEntryPoint.Instance.GetTranscodingJob(outputPath, TranscodingJobType);
|
||||
|
||||
if (job != null)
|
||||
{
|
||||
job.HasExited = true;
|
||||
}
|
||||
|
||||
Logger.Debug("Disposing stream resources");
|
||||
state.Dispose();
|
||||
|
||||
@ -1126,13 +1214,13 @@ namespace MediaBrowser.Api.Playback
|
||||
return state.VideoRequest.Framerate.Value;
|
||||
}
|
||||
|
||||
var maxrate = state.VideoRequest.MaxFramerate ?? 23.97602;
|
||||
var maxrate = state.VideoRequest.MaxFramerate;
|
||||
|
||||
if (state.VideoStream != null)
|
||||
if (maxrate.HasValue && state.VideoStream != null)
|
||||
{
|
||||
var contentRate = state.VideoStream.AverageFrameRate ?? state.VideoStream.RealFrameRate;
|
||||
|
||||
if (contentRate.HasValue && contentRate.Value > maxrate)
|
||||
if (contentRate.HasValue && contentRate.Value > maxrate.Value)
|
||||
{
|
||||
return maxrate;
|
||||
}
|
||||
@ -1330,8 +1418,6 @@ namespace MediaBrowser.Api.Playback
|
||||
ParseParams(request);
|
||||
}
|
||||
|
||||
var user = AuthorizationRequestFilterAttribute.GetCurrentUser(Request, UserManager);
|
||||
|
||||
var url = Request.PathInfo;
|
||||
|
||||
if (string.IsNullOrEmpty(request.AudioCodec))
|
||||
@ -1353,13 +1439,10 @@ namespace MediaBrowser.Api.Playback
|
||||
|
||||
var item = LibraryManager.GetItemById(request.Id);
|
||||
|
||||
if (user != null && item.GetPlayAccess(user) != PlayAccess.Full)
|
||||
{
|
||||
throw new ArgumentException(string.Format("{0} is not allowed to play media.", user.Name));
|
||||
}
|
||||
|
||||
List<MediaStream> mediaStreams = null;
|
||||
|
||||
state.ItemType = item.GetType().Name;
|
||||
|
||||
if (item is ILiveTvRecording)
|
||||
{
|
||||
var recording = await LiveTvManager.GetInternalRecording(request.Id, cancellationToken).ConfigureAwait(false);
|
||||
@ -1376,16 +1459,8 @@ namespace MediaBrowser.Api.Playback
|
||||
|
||||
mediaStreams = source.MediaStreams;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(path) && string.IsNullOrWhiteSpace(mediaUrl))
|
||||
{
|
||||
var streamInfo = await LiveTvManager.GetRecordingStream(request.Id, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
state.LiveTvStreamId = streamInfo.Id;
|
||||
mediaStreams = streamInfo.MediaStreams;
|
||||
|
||||
path = streamInfo.Path;
|
||||
mediaUrl = streamInfo.Url;
|
||||
}
|
||||
// Just to prevent this from being null and causing other methods to fail
|
||||
state.MediaPath = string.Empty;
|
||||
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
@ -1397,17 +1472,20 @@ namespace MediaBrowser.Api.Playback
|
||||
state.MediaPath = mediaUrl;
|
||||
state.InputProtocol = MediaProtocol.Http;
|
||||
}
|
||||
|
||||
state.RunTimeTicks = recording.RunTimeTicks;
|
||||
else
|
||||
{
|
||||
// No media info, so this is probably needed
|
||||
state.DeInterlace = true;
|
||||
}
|
||||
|
||||
if (recording.RecordingInfo.Status == RecordingStatus.InProgress)
|
||||
{
|
||||
await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
|
||||
state.ReadInputAtNativeFramerate = true;
|
||||
}
|
||||
|
||||
state.ReadInputAtNativeFramerate = recording.RecordingInfo.Status == RecordingStatus.InProgress;
|
||||
state.RunTimeTicks = recording.RunTimeTicks;
|
||||
|
||||
state.OutputAudioSync = "1000";
|
||||
state.DeInterlace = true;
|
||||
state.InputVideoSync = "-1";
|
||||
state.InputAudioSync = "1";
|
||||
state.InputContainer = recording.Container;
|
||||
@ -1418,40 +1496,27 @@ namespace MediaBrowser.Api.Playback
|
||||
|
||||
state.VideoType = VideoType.VideoFile;
|
||||
state.IsInputVideo = string.Equals(channel.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var streamInfo = await LiveTvManager.GetChannelStream(request.Id, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
state.LiveTvStreamId = streamInfo.Id;
|
||||
mediaStreams = streamInfo.MediaStreams;
|
||||
|
||||
if (!string.IsNullOrEmpty(streamInfo.Path))
|
||||
{
|
||||
state.MediaPath = streamInfo.Path;
|
||||
state.InputProtocol = MediaProtocol.File;
|
||||
|
||||
await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(streamInfo.Url))
|
||||
{
|
||||
state.MediaPath = streamInfo.Url;
|
||||
state.InputProtocol = MediaProtocol.Http;
|
||||
}
|
||||
mediaStreams = new List<MediaStream>();
|
||||
|
||||
state.ReadInputAtNativeFramerate = true;
|
||||
state.OutputAudioSync = "1000";
|
||||
state.DeInterlace = true;
|
||||
state.InputVideoSync = "-1";
|
||||
state.InputAudioSync = "1";
|
||||
|
||||
// Just to prevent this from being null and causing other methods to fail
|
||||
state.MediaPath = string.Empty;
|
||||
}
|
||||
else if (item is IChannelMediaItem)
|
||||
{
|
||||
var source = await GetChannelMediaInfo(request.Id, request.MediaSourceId, cancellationToken).ConfigureAwait(false);
|
||||
var mediaSource = await GetChannelMediaInfo(request.Id, request.MediaSourceId, cancellationToken).ConfigureAwait(false);
|
||||
state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
|
||||
state.InputProtocol = source.Protocol;
|
||||
state.MediaPath = source.Path;
|
||||
state.InputProtocol = mediaSource.Protocol;
|
||||
state.MediaPath = mediaSource.Path;
|
||||
state.RunTimeTicks = item.RunTimeTicks;
|
||||
state.RemoteHttpHeaders = source.RequiredHttpHeaders;
|
||||
mediaStreams = source.MediaStreams;
|
||||
state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
|
||||
state.InputBitrate = mediaSource.Bitrate;
|
||||
mediaStreams = mediaSource.MediaStreams;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -1465,6 +1530,7 @@ namespace MediaBrowser.Api.Playback
|
||||
state.MediaPath = mediaSource.Path;
|
||||
state.InputProtocol = mediaSource.Protocol;
|
||||
state.InputContainer = mediaSource.Container;
|
||||
state.InputBitrate = mediaSource.Bitrate;
|
||||
|
||||
if (item is Video)
|
||||
{
|
||||
@ -1488,16 +1554,23 @@ namespace MediaBrowser.Api.Playback
|
||||
state.RunTimeTicks = mediaSource.RunTimeTicks;
|
||||
}
|
||||
|
||||
if (string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase))
|
||||
// If it's a wtv and we don't have media info, we will probably need to deinterlace
|
||||
if (string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase) &&
|
||||
mediaStreams.Count == 0)
|
||||
{
|
||||
state.DeInterlace = true;
|
||||
}
|
||||
|
||||
if (state.InputProtocol == MediaProtocol.Rtmp)
|
||||
{
|
||||
state.ReadInputAtNativeFramerate = true;
|
||||
}
|
||||
|
||||
var videoRequest = request as VideoStreamRequest;
|
||||
|
||||
AttachMediaStreamInfo(state, mediaStreams, videoRequest, url);
|
||||
|
||||
state.SegmentLength = state.ReadInputAtNativeFramerate ? 5 : 10;
|
||||
state.SegmentLength = state.ReadInputAtNativeFramerate ? 5 : 7;
|
||||
state.HlsListSize = state.ReadInputAtNativeFramerate ? 100 : 1440;
|
||||
|
||||
var container = Path.GetExtension(state.RequestedUrl);
|
||||
@ -1574,6 +1647,8 @@ namespace MediaBrowser.Api.Playback
|
||||
{
|
||||
state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true);
|
||||
}
|
||||
|
||||
state.AllMediaStreams = mediaStreams;
|
||||
}
|
||||
|
||||
private async Task<MediaSourceInfo> GetChannelMediaInfo(string id,
|
||||
@ -1609,7 +1684,10 @@ namespace MediaBrowser.Api.Playback
|
||||
// Can't stream copy if we're burning in subtitles
|
||||
if (request.SubtitleStreamIndex.HasValue)
|
||||
{
|
||||
return false;
|
||||
if (request.SubtitleMethod == SubtitleDeliveryMethod.Encode)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Source and target codecs must match
|
||||
@ -1886,7 +1964,8 @@ namespace MediaBrowser.Api.Playback
|
||||
state.TargetPacketLength,
|
||||
state.TranscodeSeekInfo,
|
||||
state.IsTargetAnamorphic
|
||||
);
|
||||
|
||||
).FirstOrDefault() ?? string.Empty;
|
||||
}
|
||||
|
||||
foreach (var item in responseHeaders)
|
||||
@ -1911,12 +1990,6 @@ namespace MediaBrowser.Api.Playback
|
||||
/// <param name="videoRequest">The video request.</param>
|
||||
private void EnforceResolutionLimit(StreamState state, VideoStreamRequest videoRequest)
|
||||
{
|
||||
// If enabled, allow whatever the client asks for
|
||||
if (ServerConfigurationManager.Configuration.AllowVideoUpscaling)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Switch the incoming params to be ceilings rather than fixed values
|
||||
videoRequest.MaxWidth = videoRequest.MaxWidth ?? videoRequest.Width;
|
||||
videoRequest.MaxHeight = videoRequest.MaxHeight ?? videoRequest.Height;
|
||||
@ -1925,7 +1998,7 @@ namespace MediaBrowser.Api.Playback
|
||||
videoRequest.Height = null;
|
||||
}
|
||||
|
||||
protected string GetInputModifier(StreamState state)
|
||||
protected string GetInputModifier(StreamState state, bool genPts = true)
|
||||
{
|
||||
var inputModifier = string.Empty;
|
||||
|
||||
@ -1945,9 +2018,9 @@ namespace MediaBrowser.Api.Playback
|
||||
inputModifier += " " + GetFastSeekCommandLineParameter(state.Request);
|
||||
inputModifier = inputModifier.Trim();
|
||||
|
||||
if (state.VideoRequest != null)
|
||||
if (state.VideoRequest != null && genPts)
|
||||
{
|
||||
inputModifier += " -fflags genpts";
|
||||
inputModifier += " -fflags +genpts";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(state.InputAudioSync))
|
||||
|
186
MediaBrowser.Api/Playback/BifService.cs
Normal file
186
MediaBrowser.Api/Playback/BifService.cs
Normal file
@ -0,0 +1,186 @@
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Api.Playback
|
||||
{
|
||||
[Route("/Videos/{Id}/index.bif", "GET")]
|
||||
public class GetBifFile
|
||||
{
|
||||
[ApiMember(Name = "MediaSourceId", Description = "The media version id, if playing an alternate version", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string MediaSourceId { get; set; }
|
||||
|
||||
[ApiMember(Name = "MaxWidth", Description = "Optional. The maximum horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? MaxWidth { get; set; }
|
||||
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
public class BifService : BaseApiService
|
||||
{
|
||||
private readonly IServerApplicationPaths _appPaths;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public BifService(IServerApplicationPaths appPaths, ILibraryManager libraryManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem)
|
||||
{
|
||||
_appPaths = appPaths;
|
||||
_libraryManager = libraryManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
public object Get(GetBifFile request)
|
||||
{
|
||||
return ToStaticFileResult(GetBifFile(request).Result);
|
||||
}
|
||||
|
||||
private async Task<string> GetBifFile(GetBifFile request)
|
||||
{
|
||||
var widthVal = request.MaxWidth.HasValue ? request.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty;
|
||||
|
||||
var item = _libraryManager.GetItemById(request.Id);
|
||||
var mediaSources = ((IHasMediaSources)item).GetMediaSources(false).ToList();
|
||||
var mediaSource = mediaSources.FirstOrDefault(i => string.Equals(i.Id, request.MediaSourceId)) ?? mediaSources.First();
|
||||
|
||||
var path = Path.Combine(_appPaths.ImageCachePath, "bif", request.Id, request.MediaSourceId, widthVal, "index.bif");
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
var protocol = mediaSource.Protocol;
|
||||
|
||||
var inputPath = MediaEncoderHelpers.GetInputArgument(mediaSource.Path, protocol, null, mediaSource.PlayableStreamFileNames);
|
||||
|
||||
var semaphore = GetLock(path);
|
||||
|
||||
await semaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
await _mediaEncoder.ExtractVideoImagesOnInterval(inputPath, protocol, mediaSource.Video3DFormat,
|
||||
TimeSpan.FromSeconds(10), Path.GetDirectoryName(path), "img_", request.MaxWidth, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var images = new DirectoryInfo(Path.GetDirectoryName(path))
|
||||
.EnumerateFiles()
|
||||
.Where(img => string.Equals(img.Extension, ".jpg", StringComparison.Ordinal))
|
||||
.OrderBy(i => i.FullName)
|
||||
.ToList();
|
||||
|
||||
using (var fs = _fileSystem.GetFileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, true))
|
||||
{
|
||||
var magicNumber = new byte[] { 0x89, 0x42, 0x49, 0x46, 0x0d, 0x0a, 0x1a, 0x0a };
|
||||
await fs.WriteAsync(magicNumber, 0, magicNumber.Length);
|
||||
|
||||
// version
|
||||
var bytes = GetBytes(0);
|
||||
await fs.WriteAsync(bytes, 0, bytes.Length);
|
||||
|
||||
// image count
|
||||
bytes = GetBytes(images.Count);
|
||||
await fs.WriteAsync(bytes, 0, bytes.Length);
|
||||
|
||||
// interval in ms
|
||||
bytes = GetBytes(10000);
|
||||
await fs.WriteAsync(bytes, 0, bytes.Length);
|
||||
|
||||
// reserved
|
||||
for (var i = 20; i <= 63; i++)
|
||||
{
|
||||
bytes = new byte[] { 0x00 };
|
||||
await fs.WriteAsync(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
// write the bif index
|
||||
var index = 0;
|
||||
long imageOffset = 64 + (8 * images.Count) + 8;
|
||||
|
||||
foreach (var img in images)
|
||||
{
|
||||
bytes = GetBytes(index);
|
||||
await fs.WriteAsync(bytes, 0, bytes.Length);
|
||||
|
||||
bytes = GetBytes(imageOffset);
|
||||
await fs.WriteAsync(bytes, 0, bytes.Length);
|
||||
|
||||
imageOffset += img.Length;
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
bytes = new byte[] { 0xff, 0xff, 0xff, 0xff };
|
||||
await fs.WriteAsync(bytes, 0, bytes.Length);
|
||||
|
||||
bytes = GetBytes(imageOffset);
|
||||
await fs.WriteAsync(bytes, 0, bytes.Length);
|
||||
|
||||
// write the images
|
||||
foreach (var img in images)
|
||||
{
|
||||
using (var imgStream = _fileSystem.GetFileStream(img.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
|
||||
{
|
||||
await imgStream.CopyToAsync(fs).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] GetBytes(int value)
|
||||
{
|
||||
byte[] bytes = BitConverter.GetBytes(value);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private byte[] GetBytes(long value)
|
||||
{
|
||||
var intVal = Convert.ToInt32(value);
|
||||
return GetBytes(intVal);
|
||||
|
||||
//byte[] bytes = BitConverter.GetBytes(value);
|
||||
//if (BitConverter.IsLittleEndian)
|
||||
// Array.Reverse(bytes);
|
||||
//return bytes;
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<string, SemaphoreSlim> SemaphoreLocks = new ConcurrentDictionary<string, SemaphoreSlim>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lock.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
private static SemaphoreSlim GetLock(string filename)
|
||||
{
|
||||
return SemaphoreLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Api.Playback
|
||||
{
|
||||
public class EndlessStreamCopy
|
||||
{
|
||||
public async Task CopyStream(Stream source, Stream target, CancellationToken cancellationToken)
|
||||
{
|
||||
long position = 0;
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await source.CopyToAsync(target, 81920, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var fsPosition = source.Position;
|
||||
|
||||
var bytesRead = fsPosition - position;
|
||||
|
||||
//Logger.Debug("Streamed {0} bytes from file {1}", bytesRead, path);
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
position = fsPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,11 @@
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.IO;
|
||||
using System;
|
||||
@ -25,7 +22,8 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
/// </summary>
|
||||
public abstract class BaseHlsService : BaseStreamingService
|
||||
{
|
||||
protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder)
|
||||
protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder)
|
||||
: base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder)
|
||||
{
|
||||
}
|
||||
|
||||
@ -62,29 +60,33 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
/// Processes the request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <param name="isLive">if set to <c>true</c> [is live].</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
protected object ProcessRequest(StreamRequest request)
|
||||
protected object ProcessRequest(StreamRequest request, bool isLive)
|
||||
{
|
||||
return ProcessRequestAsync(request).Result;
|
||||
return ProcessRequestAsync(request, isLive).Result;
|
||||
}
|
||||
|
||||
private static readonly SemaphoreSlim FfmpegStartLock = new SemaphoreSlim(1, 1);
|
||||
/// <summary>
|
||||
/// Processes the request async.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <param name="isLive">if set to <c>true</c> [is live].</param>
|
||||
/// <returns>Task{System.Object}.</returns>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// A video bitrate is required
|
||||
/// <exception cref="ArgumentException">A video bitrate is required
|
||||
/// or
|
||||
/// An audio bitrate is required
|
||||
/// </exception>
|
||||
private async Task<object> ProcessRequestAsync(StreamRequest request)
|
||||
/// An audio bitrate is required</exception>
|
||||
private async Task<object> ProcessRequestAsync(StreamRequest request, bool isLive)
|
||||
{
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
if (isLive)
|
||||
{
|
||||
state.Request.StartTimeTicks = null;
|
||||
}
|
||||
|
||||
var playlist = state.OutputFilePath;
|
||||
|
||||
if (File.Exists(playlist))
|
||||
@ -93,7 +95,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
}
|
||||
else
|
||||
{
|
||||
await FfmpegStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (File.Exists(playlist))
|
||||
@ -113,18 +115,34 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
throw;
|
||||
}
|
||||
|
||||
await WaitForMinimumSegmentCount(playlist, GetSegmentWait(), cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
var waitCount = isLive ? 1 : GetSegmentWait();
|
||||
await WaitForMinimumSegmentCount(playlist, waitCount, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
FfmpegStartLock.Release();
|
||||
ApiEntryPoint.Instance.TranscodingStartLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
int audioBitrate;
|
||||
int videoBitrate;
|
||||
GetPlaylistBitrates(state, out audioBitrate, out videoBitrate);
|
||||
if (isLive)
|
||||
{
|
||||
//var file = request.PlaylistId + Path.GetExtension(Request.PathInfo);
|
||||
|
||||
//file = Path.Combine(ServerConfigurationManager.ApplicationPaths.TranscodingTempPath, file);
|
||||
|
||||
try
|
||||
{
|
||||
return ResultFactory.GetStaticFileResult(Request, playlist, FileShare.ReadWrite);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls);
|
||||
}
|
||||
}
|
||||
|
||||
var audioBitrate = state.OutputAudioBitrate ?? 0;
|
||||
var videoBitrate = state.OutputVideoBitrate ?? 0;
|
||||
|
||||
var appendBaselineStream = false;
|
||||
var baselineStreamBitrate = 64000;
|
||||
@ -165,37 +183,6 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
return minimumSegmentCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the playlist bitrates.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="audioBitrate">The audio bitrate.</param>
|
||||
/// <param name="videoBitrate">The video bitrate.</param>
|
||||
protected void GetPlaylistBitrates(StreamState state, out int audioBitrate, out int videoBitrate)
|
||||
{
|
||||
var audioBitrateParam = state.OutputAudioBitrate;
|
||||
var videoBitrateParam = state.OutputVideoBitrate;
|
||||
|
||||
if (!audioBitrateParam.HasValue)
|
||||
{
|
||||
if (state.AudioStream != null)
|
||||
{
|
||||
audioBitrateParam = state.AudioStream.BitRate;
|
||||
}
|
||||
}
|
||||
|
||||
if (!videoBitrateParam.HasValue)
|
||||
{
|
||||
if (state.VideoStream != null)
|
||||
{
|
||||
videoBitrateParam = state.VideoStream.BitRate;
|
||||
}
|
||||
}
|
||||
|
||||
audioBitrate = audioBitrateParam ?? 0;
|
||||
videoBitrate = videoBitrateParam ?? 0;
|
||||
}
|
||||
|
||||
private string GetMasterPlaylistFileText(string firstPlaylist, int bitrate, bool includeBaselineStream, int baselineStreamBitrate)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
@ -223,49 +210,37 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
|
||||
protected async Task WaitForMinimumSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.Debug("Waiting for {0} segments in {1}", segmentCount, playlist);
|
||||
|
||||
while (true)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
string fileText;
|
||||
|
||||
// Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
|
||||
using (var fileStream = FileSystem.GetFileStream(playlist, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
|
||||
{
|
||||
using (var reader = new StreamReader(fileStream))
|
||||
{
|
||||
fileText = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
var count = 0;
|
||||
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
var line = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||
|
||||
if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
count++;
|
||||
if (count >= segmentCount)
|
||||
{
|
||||
Logger.Debug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (CountStringOccurrences(fileText, "#EXTINF:") >= segmentCount)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await Task.Delay(25, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Count occurrences of strings.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="pattern">The pattern.</param>
|
||||
/// <returns>System.Int32.</returns>
|
||||
private static int CountStringOccurrences(string text, string pattern)
|
||||
{
|
||||
// Loop through all instances of the string 'text'.
|
||||
var count = 0;
|
||||
var i = 0;
|
||||
while ((i = text.IndexOf(pattern, i, StringComparison.OrdinalIgnoreCase)) != -1)
|
||||
{
|
||||
i += pattern.Length;
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command line arguments.
|
||||
/// </summary>
|
||||
@ -279,7 +254,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
|
||||
var itsOffsetMs = hlsVideoRequest == null
|
||||
? 0
|
||||
: ((GetHlsVideoStream)state.VideoRequest).TimeStampOffsetMs;
|
||||
: hlsVideoRequest.TimeStampOffsetMs;
|
||||
|
||||
var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds.ToString(UsCulture));
|
||||
|
||||
@ -290,7 +265,15 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
// If isEncoding is true we're actually starting ffmpeg
|
||||
var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0";
|
||||
|
||||
var args = string.Format("{0} {1} -i {2} -map_metadata -1 -threads {3} {4} {5} -sc_threshold 0 {6} -hls_time {7} -start_number {8} -hls_list_size {9} -y \"{10}\"",
|
||||
var baseUrlParam = string.Empty;
|
||||
|
||||
if (state.Request is GetLiveHlsStream)
|
||||
{
|
||||
baseUrlParam = string.Format(" -hls_base_url \"{0}/\"",
|
||||
"hls/" + Path.GetFileNameWithoutExtension(outputPath));
|
||||
}
|
||||
|
||||
var args = string.Format("{0} {1} -i {2} -map_metadata -1 -threads {3} {4} {5} -sc_threshold 0 {6} -hls_time {7} -start_number {8} -hls_list_size {9}{10} -y \"{11}\"",
|
||||
itsOffset,
|
||||
inputModifier,
|
||||
GetInputArgument(state),
|
||||
@ -301,6 +284,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
state.SegmentLength.ToString(UsCulture),
|
||||
startNumberParam,
|
||||
state.HlsListSize.ToString(UsCulture),
|
||||
baseUrlParam,
|
||||
outputPath
|
||||
).Trim();
|
||||
|
||||
|
@ -1,45 +1,46 @@
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Api.Playback.Hls
|
||||
{
|
||||
[Route("/Videos/{Id}/master.m3u8", "GET")]
|
||||
[Api(Description = "Gets a video stream using HTTP live streaming.")]
|
||||
/// <summary>
|
||||
/// Options is needed for chromecast. Threw Head in there since it's related
|
||||
/// </summary>
|
||||
[Route("/Videos/{Id}/master.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
|
||||
[Route("/Videos/{Id}/master.m3u8", "HEAD", Summary = "Gets a video stream using HTTP live streaming.")]
|
||||
public class GetMasterHlsVideoStream : VideoStreamRequest
|
||||
{
|
||||
[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; }
|
||||
public bool EnableAdaptiveBitrateStreaming { 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; }
|
||||
public GetMasterHlsVideoStream()
|
||||
{
|
||||
EnableAdaptiveBitrateStreaming = true;
|
||||
}
|
||||
}
|
||||
|
||||
[Route("/Videos/{Id}/main.m3u8", "GET")]
|
||||
[Api(Description = "Gets a video stream using HTTP live streaming.")]
|
||||
[Route("/Videos/{Id}/main.m3u8", "GET", Summary = "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>
|
||||
@ -58,34 +59,48 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
|
||||
public class DynamicHlsService : BaseHlsService
|
||||
{
|
||||
public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder)
|
||||
public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder)
|
||||
: base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder)
|
||||
{
|
||||
}
|
||||
|
||||
public object Get(GetMasterHlsVideoStream request)
|
||||
{
|
||||
var result = GetAsync(request).Result;
|
||||
var result = GetAsync(request, "GET").Result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public object Head(GetMasterHlsVideoStream request)
|
||||
{
|
||||
var result = GetAsync(request, "HEAD").Result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public object Get(GetMainHlsVideoStream request)
|
||||
{
|
||||
var result = GetPlaylistAsync(request, "main").Result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public object Get(GetDynamicHlsVideoSegment request)
|
||||
{
|
||||
if (string.Equals("baseline", request.PlaylistId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GetDynamicSegment(request, false).Result;
|
||||
}
|
||||
|
||||
return GetDynamicSegment(request, true).Result;
|
||||
return GetDynamicSegment(request, request.SegmentId).Result;
|
||||
}
|
||||
|
||||
private static readonly SemaphoreSlim FfmpegStartLock = new SemaphoreSlim(1, 1);
|
||||
private async Task<object> GetDynamicSegment(GetDynamicHlsVideoSegment request, bool isMain)
|
||||
private async Task<object> GetDynamicSegment(VideoStreamRequest request, string segmentId)
|
||||
{
|
||||
if ((request.StartTimeTicks ?? 0) > 0)
|
||||
{
|
||||
throw new ArgumentException("StartTimeTicks is not allowed.");
|
||||
}
|
||||
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = cancellationTokenSource.Token;
|
||||
|
||||
var index = int.Parse(request.SegmentId, NumberStyles.Integer, UsCulture);
|
||||
var index = int.Parse(segmentId, NumberStyles.Integer, UsCulture);
|
||||
|
||||
var state = await GetState(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@ -96,25 +111,35 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
if (File.Exists(segmentPath))
|
||||
{
|
||||
ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
|
||||
return GetSegementResult(segmentPath);
|
||||
return await GetSegmentResult(playlistPath, segmentPath, index, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await FfmpegStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (File.Exists(segmentPath))
|
||||
{
|
||||
ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
|
||||
return GetSegementResult(segmentPath);
|
||||
return await GetSegmentResult(playlistPath, segmentPath, index, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (index == 0)
|
||||
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath);
|
||||
|
||||
if (currentTranscodingIndex == null || index < currentTranscodingIndex.Value || (index - currentTranscodingIndex.Value) > 4)
|
||||
{
|
||||
// If the playlist doesn't already exist, startup ffmpeg
|
||||
try
|
||||
{
|
||||
ApiEntryPoint.Instance.KillTranscodingJobs(state.Request.DeviceId, false);
|
||||
await ApiEntryPoint.Instance.KillTranscodingJobs(j => j.Type == TranscodingJobType.Hls && string.Equals(j.DeviceId, request.DeviceId, StringComparison.OrdinalIgnoreCase), p => !string.Equals(p, playlistPath, StringComparison.OrdinalIgnoreCase), false).ConfigureAwait(false);
|
||||
|
||||
if (currentTranscodingIndex.HasValue)
|
||||
{
|
||||
DeleteLastFile(playlistPath, 0);
|
||||
}
|
||||
|
||||
var startSeconds = index * state.SegmentLength;
|
||||
request.StartTimeTicks = TimeSpan.FromSeconds(startSeconds).Ticks;
|
||||
|
||||
await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
|
||||
}
|
||||
@ -124,13 +149,13 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
throw;
|
||||
}
|
||||
|
||||
await WaitForMinimumSegmentCount(playlistPath, 2, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
FfmpegStartLock.Release();
|
||||
ApiEntryPoint.Instance.TranscodingStartLock.Release();
|
||||
}
|
||||
|
||||
Logger.Info("waiting for {0}", segmentPath);
|
||||
@ -140,14 +165,88 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
}
|
||||
|
||||
Logger.Info("returning {0}", segmentPath);
|
||||
return GetSegementResult(segmentPath);
|
||||
return await GetSegmentResult(playlistPath, segmentPath, index, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public int? GetCurrentTranscodingIndex(string playlist)
|
||||
{
|
||||
var file = GetLastTranscodingFile(playlist, FileSystem);
|
||||
|
||||
if (file == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
|
||||
|
||||
var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
|
||||
|
||||
return int.Parse(indexString, NumberStyles.Integer, UsCulture);
|
||||
}
|
||||
|
||||
private void DeleteLastFile(string path, int retryCount)
|
||||
{
|
||||
if (retryCount >= 5)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var file = GetLastTranscodingFile(path, FileSystem);
|
||||
|
||||
if (file != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(file.FullName);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, file.FullName);
|
||||
|
||||
Thread.Sleep(100);
|
||||
DeleteLastFile(path, retryCount + 1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, file.FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static FileInfo GetLastTranscodingFile(string playlist, IFileSystem fileSystem)
|
||||
{
|
||||
var folder = Path.GetDirectoryName(playlist);
|
||||
|
||||
try
|
||||
{
|
||||
return new DirectoryInfo(folder)
|
||||
.EnumerateFiles("*", SearchOption.TopDirectoryOnly)
|
||||
.Where(i => string.Equals(i.Extension, ".ts", StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(fileSystem.GetLastWriteTimeUtc)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override int GetStartNumber(StreamState state)
|
||||
{
|
||||
var request = (GetDynamicHlsVideoSegment) state.Request;
|
||||
return GetStartNumber(state.VideoRequest);
|
||||
}
|
||||
|
||||
return int.Parse(request.SegmentId, NumberStyles.Integer, UsCulture);
|
||||
private int GetStartNumber(VideoStreamRequest request)
|
||||
{
|
||||
var segmentId = "0";
|
||||
|
||||
var segmentRequest = request as GetDynamicHlsVideoSegment;
|
||||
if (segmentRequest != null)
|
||||
{
|
||||
segmentId = segmentRequest.SegmentId;
|
||||
}
|
||||
|
||||
return int.Parse(segmentId, NumberStyles.Integer, UsCulture);
|
||||
}
|
||||
|
||||
private string GetSegmentPath(string playlist, int index)
|
||||
@ -159,75 +258,262 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
return Path.Combine(folder, filename + index.ToString(UsCulture) + ".ts");
|
||||
}
|
||||
|
||||
private object GetSegementResult(string path)
|
||||
private async Task<object> GetSegmentResult(string playlistPath, string segmentPath, int segmentIndex, CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO: Handle if it's currently being written to
|
||||
return ResultFactory.GetStaticFileResult(Request, path, FileShare.ReadWrite);
|
||||
// If all transcoding has completed, just return immediately
|
||||
if (!IsTranscoding(playlistPath))
|
||||
{
|
||||
return ResultFactory.GetStaticFileResult(Request, segmentPath, FileShare.ReadWrite);
|
||||
}
|
||||
|
||||
var segmentFilename = Path.GetFileName(segmentPath);
|
||||
|
||||
using (var fileStream = FileSystem.GetFileStream(playlistPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
|
||||
{
|
||||
using (var reader = new StreamReader(fileStream))
|
||||
{
|
||||
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
|
||||
// If it appears in the playlist, it's done
|
||||
if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
return ResultFactory.GetStaticFileResult(Request, segmentPath, FileShare.ReadWrite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if a different file is encoding, it's done
|
||||
//var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath);
|
||||
//if (currentTranscodingIndex > segmentIndex)
|
||||
//{
|
||||
// return ResultFactory.GetStaticFileResult(Request, segmentPath, FileShare.ReadWrite);
|
||||
//}
|
||||
|
||||
// Wait for the file to stop being written to, then stream it
|
||||
var length = new FileInfo(segmentPath).Length;
|
||||
var eofCount = 0;
|
||||
|
||||
while (eofCount < 10)
|
||||
{
|
||||
var info = new FileInfo(segmentPath);
|
||||
|
||||
if (!info.Exists)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var newLength = info.Length;
|
||||
|
||||
if (newLength == length)
|
||||
{
|
||||
eofCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
eofCount = 0;
|
||||
}
|
||||
|
||||
length = newLength;
|
||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return ResultFactory.GetStaticFileResult(Request, segmentPath, FileShare.ReadWrite);
|
||||
}
|
||||
|
||||
private async Task<object> GetAsync(GetMasterHlsVideoStream request)
|
||||
private bool IsTranscoding(string playlistPath)
|
||||
{
|
||||
var job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType);
|
||||
|
||||
return job != null && !job.HasExited;
|
||||
}
|
||||
|
||||
private async Task<object> GetAsync(GetMasterHlsVideoStream request, string method)
|
||||
{
|
||||
var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
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)
|
||||
if (string.Equals(request.AudioCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
appendBaselineStream = hlsVideoRequest.AppendBaselineStream;
|
||||
baselineStreamBitrate = hlsVideoRequest.BaselineStreamAudioBitRate ?? baselineStreamBitrate;
|
||||
throw new ArgumentException("Audio codec copy is not allowed here.");
|
||||
}
|
||||
|
||||
var playlistText = GetMasterPlaylistFileText(videoBitrate + audioBitrate, appendBaselineStream, baselineStreamBitrate);
|
||||
if (string.Equals(request.VideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException("Video codec copy is not allowed here.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(request.MediaSourceId))
|
||||
{
|
||||
throw new ArgumentException("MediaSourceId is required");
|
||||
}
|
||||
|
||||
var playlistText = string.Empty;
|
||||
|
||||
if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var audioBitrate = state.OutputAudioBitrate ?? 0;
|
||||
var videoBitrate = state.OutputVideoBitrate ?? 0;
|
||||
|
||||
playlistText = GetMasterPlaylistFileText(state, videoBitrate + audioBitrate);
|
||||
}
|
||||
|
||||
return ResultFactory.GetResult(playlistText, Common.Net.MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
|
||||
}
|
||||
|
||||
private string GetMasterPlaylistFileText(int bitrate, bool includeBaselineStream, int baselineStreamBitrate)
|
||||
private string GetMasterPlaylistFileText(StreamState state, int totalBitrate)
|
||||
{
|
||||
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);
|
||||
var playlistUrl = (state.RunTimeTicks ?? 0) > 0 ? "main.m3u8" : "live.m3u8";
|
||||
playlistUrl += queryString;
|
||||
|
||||
// Low bitrate stream
|
||||
if (includeBaselineStream)
|
||||
var request = (GetMasterHlsVideoStream)state.Request;
|
||||
|
||||
var subtitleStreams = state.AllMediaStreams
|
||||
.Where(i => i.IsTextSubtitleStream)
|
||||
.ToList();
|
||||
|
||||
var subtitleGroup = subtitleStreams.Count > 0 && request.SubtitleMethod == SubtitleDeliveryMethod.Hls ?
|
||||
"subs" :
|
||||
null;
|
||||
|
||||
AppendPlaylist(builder, playlistUrl, totalBitrate, subtitleGroup);
|
||||
|
||||
if (EnableAdaptiveBitrateStreaming(state))
|
||||
{
|
||||
builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + baselineStreamBitrate.ToString(UsCulture));
|
||||
playlistUrl = "baseline.m3u8" + queryString;
|
||||
builder.AppendLine(playlistUrl);
|
||||
var requestedVideoBitrate = state.VideoRequest.VideoBitRate.Value;
|
||||
|
||||
// By default, vary by just 200k
|
||||
var variation = GetBitrateVariation(totalBitrate);
|
||||
|
||||
var newBitrate = totalBitrate - variation;
|
||||
var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, (requestedVideoBitrate - variation));
|
||||
AppendPlaylist(builder, variantUrl, newBitrate, subtitleGroup);
|
||||
|
||||
variation *= 2;
|
||||
newBitrate = totalBitrate - variation;
|
||||
variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, (requestedVideoBitrate - variation));
|
||||
AppendPlaylist(builder, variantUrl, newBitrate, subtitleGroup);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(subtitleGroup))
|
||||
{
|
||||
AddSubtitles(state, subtitleStreams, builder);
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public object Get(GetMainHlsVideoStream request)
|
||||
private string ReplaceBitrate(string url, int oldValue, int newValue)
|
||||
{
|
||||
var result = GetPlaylistAsync(request, "main").Result;
|
||||
|
||||
return result;
|
||||
return url.Replace(
|
||||
"videobitrate=" + oldValue.ToString(UsCulture),
|
||||
"videobitrate=" + newValue.ToString(UsCulture),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public object Get(GetBaselineHlsVideoStream request)
|
||||
private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder)
|
||||
{
|
||||
var result = GetPlaylistAsync(request, "baseline").Result;
|
||||
var selectedIndex = state.SubtitleStream == null ? (int?)null : state.SubtitleStream.Index;
|
||||
|
||||
return result;
|
||||
foreach (var stream in subtitles)
|
||||
{
|
||||
const string format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},URI=\"{3}\",LANGUAGE=\"{4}\"";
|
||||
|
||||
var name = stream.Language;
|
||||
|
||||
var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
|
||||
var isForced = stream.IsForced;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name)) name = stream.Codec ?? "Unknown";
|
||||
|
||||
var url = string.Format("{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}",
|
||||
state.Request.MediaSourceId,
|
||||
stream.Index.ToString(UsCulture),
|
||||
30.ToString(UsCulture));
|
||||
|
||||
var line = string.Format(format,
|
||||
name,
|
||||
isDefault ? "YES" : "NO",
|
||||
isForced ? "YES" : "NO",
|
||||
url,
|
||||
stream.Language ?? "Unknown");
|
||||
|
||||
builder.AppendLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
private bool EnableAdaptiveBitrateStreaming(StreamState state)
|
||||
{
|
||||
var request = state.Request as GetMasterHlsVideoStream;
|
||||
|
||||
if (request != null && !request.EnableAdaptiveBitrateStreaming)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(state.MediaPath))
|
||||
{
|
||||
// Opening live streams is so slow it's not even worth it
|
||||
return false;
|
||||
}
|
||||
|
||||
return state.VideoRequest.VideoBitRate.HasValue;
|
||||
}
|
||||
|
||||
private void AppendPlaylist(StringBuilder builder, string url, int bitrate, string subtitleGroup)
|
||||
{
|
||||
var header = "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + bitrate.ToString(UsCulture);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(subtitleGroup))
|
||||
{
|
||||
header += string.Format(",SUBTITLES=\"{0}\"", subtitleGroup);
|
||||
}
|
||||
|
||||
builder.AppendLine(header);
|
||||
builder.AppendLine(url);
|
||||
}
|
||||
|
||||
private int GetBitrateVariation(int bitrate)
|
||||
{
|
||||
// By default, vary by just 50k
|
||||
var variation = 50000;
|
||||
|
||||
if (bitrate >= 10000000)
|
||||
{
|
||||
variation = 2000000;
|
||||
}
|
||||
else if (bitrate >= 5000000)
|
||||
{
|
||||
variation = 1500000;
|
||||
}
|
||||
else if (bitrate >= 3000000)
|
||||
{
|
||||
variation = 1000000;
|
||||
}
|
||||
else if (bitrate >= 2000000)
|
||||
{
|
||||
variation = 500000;
|
||||
}
|
||||
else if (bitrate >= 1000000)
|
||||
{
|
||||
variation = 300000;
|
||||
}
|
||||
else if (bitrate >= 600000)
|
||||
{
|
||||
variation = 200000;
|
||||
}
|
||||
else if (bitrate >= 400000)
|
||||
{
|
||||
variation = 100000;
|
||||
}
|
||||
|
||||
return variation;
|
||||
}
|
||||
|
||||
private async Task<object> GetPlaylistAsync(VideoStreamRequest request, string name)
|
||||
@ -240,6 +526,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
builder.AppendLine("#EXT-X-VERSION:3");
|
||||
builder.AppendLine("#EXT-X-TARGETDURATION:" + state.SegmentLength.ToString(UsCulture));
|
||||
builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
|
||||
builder.AppendLine("#EXT-X-ALLOW-CACHE:NO");
|
||||
|
||||
var queryStringIndex = Request.RawUrl.IndexOf('?');
|
||||
var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
|
||||
@ -252,7 +539,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
{
|
||||
var length = seconds >= state.SegmentLength ? state.SegmentLength : seconds;
|
||||
|
||||
builder.AppendLine("#EXTINF:" + length.ToString(UsCulture));
|
||||
builder.AppendLine("#EXTINF:" + length.ToString(UsCulture) + ",");
|
||||
|
||||
builder.AppendLine(string.Format("hlsdynamic/{0}/{1}.ts{2}",
|
||||
|
||||
@ -312,9 +599,8 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
return IsH264(state.VideoStream) ? "-codec:v:0 copy -bsf h264_mp4toannexb" : "-codec:v:0 copy";
|
||||
}
|
||||
|
||||
var keyFrameArg = state.ReadInputAtNativeFramerate ?
|
||||
" -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+1))" :
|
||||
" -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+5))";
|
||||
var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})",
|
||||
state.SegmentLength.ToString(UsCulture));
|
||||
|
||||
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream;
|
||||
|
||||
@ -323,18 +609,50 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
// Add resolution params, if specified
|
||||
if (!hasGraphicalSubs)
|
||||
{
|
||||
args += GetOutputSizeParam(state, codec, CancellationToken.None);
|
||||
args += GetOutputSizeParam(state, codec, false);
|
||||
}
|
||||
|
||||
// This is for internal graphical subs
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
args += GetInternalGraphicalSubtitleParam(state, codec);
|
||||
args += GetGraphicalSubtitleParam(state, codec);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command line arguments.
|
||||
/// </summary>
|
||||
/// <param name="outputPath">The output path.</param>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="isEncoding">if set to <c>true</c> [is encoding].</param>
|
||||
/// <returns>System.String.</returns>
|
||||
protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
|
||||
{
|
||||
var threads = GetNumberOfThreads(state, false);
|
||||
|
||||
var inputModifier = GetInputModifier(state);
|
||||
|
||||
// If isEncoding is true we're actually starting ffmpeg
|
||||
var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0";
|
||||
|
||||
var args = string.Format("{0} -i {1} -map_metadata -1 -threads {2} {3} {4} -copyts -flags -global_header {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"",
|
||||
inputModifier,
|
||||
GetInputArgument(state),
|
||||
threads,
|
||||
GetMapArgs(state),
|
||||
GetVideoArguments(state),
|
||||
GetAudioArguments(state),
|
||||
state.SegmentLength.ToString(UsCulture),
|
||||
startNumberParam,
|
||||
state.HlsListSize.ToString(UsCulture),
|
||||
outputPath
|
||||
).Trim();
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the segment file extension.
|
||||
/// </summary>
|
||||
@ -344,5 +662,13 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
{
|
||||
return ".ts";
|
||||
}
|
||||
|
||||
protected override TranscodingJobType TranscodingJobType
|
||||
{
|
||||
get
|
||||
{
|
||||
return TranscodingJobType.Hls;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,9 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
|
||||
public void Delete(StopEncodingProcess request)
|
||||
{
|
||||
ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, true);
|
||||
var task = ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, path => true, true);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -10,7 +10,6 @@ using ServiceStack;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Api.Playback.Hls
|
||||
@ -32,6 +31,12 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
public int TimeStampOffsetMs { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Videos/{Id}/live.m3u8", "GET")]
|
||||
[Api(Description = "Gets a video stream using HTTP live streaming.")]
|
||||
public class GetLiveHlsStream : VideoStreamRequest
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class GetHlsVideoSegment
|
||||
/// </summary>
|
||||
@ -105,7 +110,12 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
/// <returns>System.Object.</returns>
|
||||
public object Get(GetHlsVideoStream request)
|
||||
{
|
||||
return ProcessRequest(request);
|
||||
return ProcessRequest(request, false);
|
||||
}
|
||||
|
||||
public object Get(GetLiveHlsStream request)
|
||||
{
|
||||
return ProcessRequest(request, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -159,9 +169,8 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
return IsH264(state.VideoStream) ? "-codec:v:0 copy -bsf h264_mp4toannexb" : "-codec:v:0 copy";
|
||||
}
|
||||
|
||||
var keyFrameArg = state.ReadInputAtNativeFramerate ?
|
||||
" -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+1))" :
|
||||
" -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+5))";
|
||||
var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})",
|
||||
state.SegmentLength.ToString(UsCulture));
|
||||
|
||||
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream;
|
||||
|
||||
@ -170,13 +179,13 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
// Add resolution params, if specified
|
||||
if (!hasGraphicalSubs)
|
||||
{
|
||||
args += GetOutputSizeParam(state, codec, CancellationToken.None);
|
||||
args += GetOutputSizeParam(state, codec);
|
||||
}
|
||||
|
||||
// This is for internal graphical subs
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
args += GetInternalGraphicalSubtitleParam(state, codec);
|
||||
args += GetGraphicalSubtitleParam(state, codec);
|
||||
}
|
||||
|
||||
return args;
|
||||
|
@ -7,6 +7,7 @@ using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using ServiceStack.Web;
|
||||
@ -26,7 +27,8 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
protected readonly IImageProcessor ImageProcessor;
|
||||
protected readonly IHttpClient HttpClient;
|
||||
|
||||
protected BaseProgressiveStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder)
|
||||
protected BaseProgressiveStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IImageProcessor imageProcessor, IHttpClient httpClient)
|
||||
: base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder)
|
||||
{
|
||||
ImageProcessor = imageProcessor;
|
||||
HttpClient = httpClient;
|
||||
@ -53,22 +55,22 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
{
|
||||
var videoCodec = state.VideoRequest.VideoCodec;
|
||||
|
||||
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ".ts";
|
||||
}
|
||||
if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ".ogv";
|
||||
}
|
||||
if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ".webm";
|
||||
}
|
||||
if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ".asf";
|
||||
}
|
||||
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ".ts";
|
||||
}
|
||||
if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ".ogv";
|
||||
}
|
||||
if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ".webm";
|
||||
}
|
||||
if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ".asf";
|
||||
}
|
||||
}
|
||||
|
||||
// Try to infer based on the desired audio codec
|
||||
@ -114,7 +116,9 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
/// <returns>Task.</returns>
|
||||
protected object ProcessRequest(StreamRequest request, bool isHeadRequest)
|
||||
{
|
||||
var state = GetState(request, CancellationToken.None).Result;
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
var state = GetState(request, cancellationTokenSource.Token).Result;
|
||||
|
||||
var responseHeaders = new Dictionary<string, string>();
|
||||
|
||||
@ -123,13 +127,9 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
{
|
||||
AddDlnaHeaders(state, responseHeaders, true);
|
||||
|
||||
try
|
||||
using (state)
|
||||
{
|
||||
return GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest).Result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
state.Dispose();
|
||||
return GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).Result;
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,13 +151,24 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
{
|
||||
var contentType = state.GetMimeType(state.MediaPath);
|
||||
|
||||
try
|
||||
using (state)
|
||||
{
|
||||
return ResultFactory.GetStaticFileResult(Request, state.MediaPath, contentType, FileShare.Read, responseHeaders, isHeadRequest);
|
||||
}
|
||||
finally
|
||||
{
|
||||
state.Dispose();
|
||||
var throttleLimit = state.InputBitrate.HasValue ? (state.InputBitrate.Value / 8) : 0;
|
||||
|
||||
return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
|
||||
{
|
||||
ResponseHeaders = responseHeaders,
|
||||
ContentType = contentType,
|
||||
IsHeadRequest = isHeadRequest,
|
||||
Path = state.MediaPath,
|
||||
Throttle = request.Throttle,
|
||||
|
||||
// Pad by 20% to play it safe
|
||||
ThrottleLimit = Convert.ToInt64(1.2 * throttleLimit),
|
||||
|
||||
// Three minutes
|
||||
MinThrottlePosition = throttleLimit * 180
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,7 +179,13 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
|
||||
try
|
||||
{
|
||||
return ResultFactory.GetStaticFileResult(Request, outputPath, contentType, FileShare.Read, responseHeaders, isHeadRequest);
|
||||
return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
|
||||
{
|
||||
ResponseHeaders = responseHeaders,
|
||||
ContentType = contentType,
|
||||
IsHeadRequest = isHeadRequest,
|
||||
Path = outputPath
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -179,7 +196,7 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
// Need to start ffmpeg
|
||||
try
|
||||
{
|
||||
return GetStreamResult(state, responseHeaders, isHeadRequest).Result;
|
||||
return GetStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).Result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -195,8 +212,9 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="responseHeaders">The response headers.</param>
|
||||
/// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
|
||||
/// <param name="cancellationTokenSource">The cancellation token source.</param>
|
||||
/// <returns>Task{System.Object}.</returns>
|
||||
private async Task<object> GetStaticRemoteStreamResult(StreamState state, Dictionary<string, string> responseHeaders, bool isHeadRequest)
|
||||
private async Task<object> GetStaticRemoteStreamResult(StreamState state, Dictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
|
||||
{
|
||||
string useragent = null;
|
||||
state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent);
|
||||
@ -205,7 +223,8 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
{
|
||||
Url = state.MediaPath,
|
||||
UserAgent = useragent,
|
||||
BufferContent = false
|
||||
BufferContent = false,
|
||||
CancellationToken = cancellationTokenSource.Token
|
||||
};
|
||||
|
||||
var response = await HttpClient.GetResponse(options).ConfigureAwait(false);
|
||||
@ -246,8 +265,9 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="responseHeaders">The response headers.</param>
|
||||
/// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
|
||||
/// <param name="cancellationTokenSource">The cancellation token source.</param>
|
||||
/// <returns>Task{System.Object}.</returns>
|
||||
private async Task<object> GetStreamResult(StreamState state, IDictionary<string, string> responseHeaders, bool isHeadRequest)
|
||||
private async Task<object> GetStreamResult(StreamState state, IDictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
|
||||
{
|
||||
// Use the command line args with a dummy playlist path
|
||||
var outputPath = state.OutputFilePath;
|
||||
@ -283,27 +303,36 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
return streamResult;
|
||||
}
|
||||
|
||||
if (!File.Exists(outputPath))
|
||||
await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await StartFfMpeg(state, outputPath, new CancellationTokenSource()).ConfigureAwait(false);
|
||||
if (!File.Exists(outputPath))
|
||||
{
|
||||
await StartFfMpeg(state, outputPath, cancellationTokenSource).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApiEntryPoint.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
|
||||
state.Dispose();
|
||||
}
|
||||
|
||||
var job = ApiEntryPoint.Instance.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
|
||||
var result = new ProgressiveStreamWriter(outputPath, Logger, FileSystem, job);
|
||||
|
||||
result.Options["Content-Type"] = contentType;
|
||||
|
||||
// Add the response headers to the result object
|
||||
foreach (var item in responseHeaders)
|
||||
{
|
||||
result.Options[item.Key] = item.Value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
else
|
||||
finally
|
||||
{
|
||||
ApiEntryPoint.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
|
||||
state.Dispose();
|
||||
ApiEntryPoint.Instance.TranscodingStartLock.Release();
|
||||
}
|
||||
|
||||
var result = new ProgressiveStreamWriter(outputPath, Logger, FileSystem);
|
||||
|
||||
result.Options["Content-Type"] = contentType;
|
||||
|
||||
// Add the response headers to the result object
|
||||
foreach (var item in responseHeaders)
|
||||
{
|
||||
result.Options[item.Key] = item.Value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,7 +1,7 @@
|
||||
using MediaBrowser.Common.IO;
|
||||
using System;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using ServiceStack.Web;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
@ -13,6 +13,7 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
private string Path { get; set; }
|
||||
private ILogger Logger { get; set; }
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly TranscodingJob _job;
|
||||
|
||||
/// <summary>
|
||||
/// The _options
|
||||
@ -33,11 +34,12 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="fileSystem">The file system.</param>
|
||||
public ProgressiveStreamWriter(string path, ILogger logger, IFileSystem fileSystem)
|
||||
public ProgressiveStreamWriter(string path, ILogger logger, IFileSystem fileSystem, TranscodingJob job)
|
||||
{
|
||||
Path = path;
|
||||
Logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_job = job;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -60,11 +62,12 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
{
|
||||
try
|
||||
{
|
||||
await StreamFile(Path, responseStream).ConfigureAwait(false);
|
||||
await new ProgressiveFileCopier(_fileSystem, _job)
|
||||
.StreamFile(Path, responseStream).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Error streaming media. The client has most likely disconnected or transcoding has failed.");
|
||||
Logger.ErrorException("Error streaming media. The client has most likely disconnected or transcoding has failed.", ex);
|
||||
|
||||
throw;
|
||||
}
|
||||
@ -73,14 +76,20 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
ApiEntryPoint.Instance.OnTranscodeEndRequest(Path, TranscodingJobType.Progressive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Streams the file.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="outputStream">The output stream.</param>
|
||||
/// <returns>Task{System.Boolean}.</returns>
|
||||
private async Task StreamFile(string path, Stream outputStream)
|
||||
public class ProgressiveFileCopier
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly TranscodingJob _job;
|
||||
|
||||
public ProgressiveFileCopier(IFileSystem fileSystem, TranscodingJob job)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_job = job;
|
||||
}
|
||||
|
||||
public async Task StreamFile(string path, Stream outputStream)
|
||||
{
|
||||
var eofCount = 0;
|
||||
long position = 0;
|
||||
@ -99,7 +108,10 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
eofCount++;
|
||||
if (_job == null || _job.HasExited)
|
||||
{
|
||||
eofCount++;
|
||||
}
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
|
@ -11,7 +11,6 @@ using MediaBrowser.Model.IO;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
namespace MediaBrowser.Api.Playback.Progressive
|
||||
{
|
||||
@ -144,7 +143,8 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
return state.VideoStream != null && IsH264(state.VideoStream) ? args + " -bsf h264_mp4toannexb" : args;
|
||||
}
|
||||
|
||||
const string keyFrameArg = " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+5))";
|
||||
var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})",
|
||||
5.ToString(UsCulture));
|
||||
|
||||
args += keyFrameArg;
|
||||
|
||||
@ -153,7 +153,7 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
// Add resolution params, if specified
|
||||
if (!hasGraphicalSubs)
|
||||
{
|
||||
args += GetOutputSizeParam(state, codec, CancellationToken.None);
|
||||
args += GetOutputSizeParam(state, codec);
|
||||
}
|
||||
|
||||
var qualityParam = GetVideoQualityParam(state, codec, false);
|
||||
@ -166,7 +166,7 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
// This is for internal graphical subs
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
args += GetInternalGraphicalSubtitleParam(state, codec);
|
||||
args += GetGraphicalSubtitleParam(state, codec);
|
||||
}
|
||||
|
||||
return args;
|
||||
@ -210,7 +210,7 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
args += " -ab " + bitrate.Value.ToString(UsCulture);
|
||||
}
|
||||
|
||||
args += " " + GetAudioFilterParam(state, true);
|
||||
args += " " + GetAudioFilterParam(state, false);
|
||||
|
||||
return args;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using ServiceStack;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using ServiceStack;
|
||||
|
||||
namespace MediaBrowser.Api.Playback
|
||||
{
|
||||
@ -69,6 +70,8 @@ namespace MediaBrowser.Api.Playback
|
||||
public string DeviceProfileId { get; set; }
|
||||
|
||||
public string Params { get; set; }
|
||||
|
||||
public bool Throttle { get; set; }
|
||||
}
|
||||
|
||||
public class VideoStreamRequest : StreamRequest
|
||||
@ -160,6 +163,9 @@ namespace MediaBrowser.Api.Playback
|
||||
[ApiMember(Name = "Level", Description = "Optional. Specify a level for the h264 profile, e.g. 3, 3.1.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string Level { get; set; }
|
||||
|
||||
[ApiMember(Name = "SubtitleDeliveryMethod", Description = "Optional. Specify the subtitle delivery method.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public SubtitleDeliveryMethod SubtitleMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance has fixed resolution.
|
||||
/// </summary>
|
||||
|
@ -38,6 +38,8 @@ namespace MediaBrowser.Api.Playback
|
||||
|
||||
public string InputContainer { get; set; }
|
||||
|
||||
public List<MediaStream> AllMediaStreams { get; set; }
|
||||
|
||||
public MediaStream AudioStream { get; set; }
|
||||
public MediaStream VideoStream { get; set; }
|
||||
public MediaStream SubtitleStream { get; set; }
|
||||
@ -66,6 +68,8 @@ namespace MediaBrowser.Api.Playback
|
||||
|
||||
public long? RunTimeTicks;
|
||||
|
||||
public long? InputBitrate { get; set; }
|
||||
|
||||
public string OutputAudioSync = "1";
|
||||
public string OutputVideoSync = "vfr";
|
||||
|
||||
@ -78,6 +82,7 @@ namespace MediaBrowser.Api.Playback
|
||||
SupportedAudioCodecs = new List<string>();
|
||||
PlayableStreamFileNames = new List<string>();
|
||||
RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
AllMediaStreams = new List<MediaStream>();
|
||||
}
|
||||
|
||||
public string InputAudioSync { get; set; }
|
||||
@ -94,6 +99,10 @@ namespace MediaBrowser.Api.Playback
|
||||
public bool EnableMpegtsM2TsMode { get; set; }
|
||||
public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
|
||||
|
||||
public long? EncodingDurationTicks { get; set; }
|
||||
|
||||
public string ItemType { get; set; }
|
||||
|
||||
public string GetMimeType(string outputPath)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(MimeType))
|
||||
|
176
MediaBrowser.Api/PlaylistService.cs
Normal file
176
MediaBrowser.Api/PlaylistService.cs
Normal file
@ -0,0 +1,176 @@
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Playlists;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Api
|
||||
{
|
||||
[Route("/Playlists", "POST", Summary = "Creates a new playlist")]
|
||||
public class CreatePlaylist : IReturn<PlaylistCreationResult>
|
||||
{
|
||||
[ApiMember(Name = "Name", Description = "The name of the new playlist.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[ApiMember(Name = "Ids", Description = "Item Ids to add to the playlist", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
|
||||
public string Ids { get; set; }
|
||||
|
||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string UserId { get; set; }
|
||||
|
||||
[ApiMember(Name = "MediaType", Description = "The playlist media type", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string MediaType { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Playlists/{Id}/Items", "POST", Summary = "Adds items to a playlist")]
|
||||
public class AddToPlaylist : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Ids", Description = "Item id, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string Ids { get; set; }
|
||||
|
||||
[ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
/// <value>The user id.</value>
|
||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string UserId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Playlists/{Id}/Items", "DELETE", Summary = "Removes items from a playlist")]
|
||||
public class RemoveFromPlaylist : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[ApiMember(Name = "EntryIds", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
|
||||
public string EntryIds { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Playlists/{Id}/Items", "GET", Summary = "Gets the original items of a playlist")]
|
||||
public class GetPlaylistItems : IReturn<QueryResult<BaseItemDto>>, IHasItemFields
|
||||
{
|
||||
[ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
/// <value>The user id.</value>
|
||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Skips over a given number of items within the results. Use for paging.
|
||||
/// </summary>
|
||||
/// <value>The start index.</value>
|
||||
[ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? StartIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of items to return
|
||||
/// </summary>
|
||||
/// <value>The limit.</value>
|
||||
[ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? Limit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Fields to return within the items, in addition to basic information
|
||||
/// </summary>
|
||||
/// <value>The fields.</value>
|
||||
[ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, CriticRatingSummary, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
|
||||
public string Fields { get; set; }
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class PlaylistService : BaseApiService
|
||||
{
|
||||
private readonly IPlaylistManager _playlistManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
public PlaylistService(IDtoService dtoService, IPlaylistManager playlistManager, IUserManager userManager, ILibraryManager libraryManager)
|
||||
{
|
||||
_dtoService = dtoService;
|
||||
_playlistManager = playlistManager;
|
||||
_userManager = userManager;
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
public async Task<object> Post(CreatePlaylist request)
|
||||
{
|
||||
var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
|
||||
{
|
||||
Name = request.Name,
|
||||
ItemIdList = (request.Ids ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(),
|
||||
UserId = request.UserId,
|
||||
MediaType = request.MediaType
|
||||
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public void Post(AddToPlaylist request)
|
||||
{
|
||||
var task = _playlistManager.AddToPlaylist(request.Id, request.Ids.Split(','), request.UserId);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
public void Delete(RemoveFromPlaylist request)
|
||||
{
|
||||
var task = _playlistManager.RemoveFromPlaylist(request.Id, request.EntryIds.Split(','));
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
public object Get(GetPlaylistItems request)
|
||||
{
|
||||
var playlist = (Playlist)_libraryManager.GetItemById(request.Id);
|
||||
var user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(new Guid(request.UserId)) : null;
|
||||
|
||||
var items = playlist.GetManageableItems().ToArray();
|
||||
|
||||
var count = items.Length;
|
||||
|
||||
if (request.StartIndex.HasValue)
|
||||
{
|
||||
items = items.Skip(request.StartIndex.Value).ToArray();
|
||||
}
|
||||
|
||||
if (request.Limit.HasValue)
|
||||
{
|
||||
items = items.Take(request.Limit.Value).ToArray();
|
||||
}
|
||||
|
||||
var dtos = items
|
||||
.Select(i => _dtoService.GetBaseItemDto(i.Item2, request.GetItemFields().ToList(), user))
|
||||
.ToArray();
|
||||
|
||||
var index = 0;
|
||||
foreach (var item in dtos)
|
||||
{
|
||||
item.PlaylistItemId = items[index].Item1.Id;
|
||||
index++;
|
||||
}
|
||||
|
||||
var result = new ItemsResult
|
||||
{
|
||||
Items = dtos,
|
||||
TotalRecordCount = count
|
||||
};
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Security;
|
||||
using MediaBrowser.Common.Updates;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
@ -100,6 +101,7 @@ namespace MediaBrowser.Api
|
||||
/// <summary>
|
||||
/// Class PluginsService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class PluginService : BaseApiService
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -1,5 +1,6 @@
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.ScheduledTasks;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using ServiceStack;
|
||||
using ServiceStack.Text.Controller;
|
||||
@ -78,6 +79,7 @@ namespace MediaBrowser.Api.ScheduledTasks
|
||||
/// <summary>
|
||||
/// Class ScheduledTasksService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class ScheduledTaskService : BaseApiService
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -4,6 +4,7 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Search;
|
||||
using ServiceStack;
|
||||
@ -79,6 +80,7 @@ namespace MediaBrowser.Api
|
||||
/// <summary>
|
||||
/// Class SearchService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class SearchService : BaseApiService
|
||||
{
|
||||
/// <summary>
|
||||
@ -109,9 +111,9 @@ namespace MediaBrowser.Api
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
public object Get(GetSearchHints request)
|
||||
public async Task<object> Get(GetSearchHints request)
|
||||
{
|
||||
var result = GetSearchHintsAsync(request).Result;
|
||||
var result = await GetSearchHintsAsync(request).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
}
|
||||
@ -192,14 +194,14 @@ namespace MediaBrowser.Api
|
||||
{
|
||||
result.Series = season.Series.Name;
|
||||
|
||||
result.EpisodeCount = season.GetRecursiveChildren(i => i is Episode).Count;
|
||||
result.EpisodeCount = season.GetRecursiveChildren().Count(i => i is Episode);
|
||||
}
|
||||
|
||||
var series = item as Series;
|
||||
|
||||
if (series != null)
|
||||
{
|
||||
result.EpisodeCount = series.GetRecursiveChildren(i => i is Episode).Count;
|
||||
result.EpisodeCount = series.GetRecursiveChildren().Count(i => i is Episode);
|
||||
}
|
||||
|
||||
var album = item as MusicAlbum;
|
||||
|
@ -7,7 +7,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Api.WebSocket
|
||||
namespace MediaBrowser.Api.Session
|
||||
{
|
||||
/// <summary>
|
||||
/// Class SessionInfoWebSocketListener
|
@ -1,4 +1,6 @@
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Security;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Session;
|
||||
using ServiceStack;
|
||||
@ -8,12 +10,13 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Api
|
||||
namespace MediaBrowser.Api.Session
|
||||
{
|
||||
/// <summary>
|
||||
/// Class GetSessions
|
||||
/// </summary>
|
||||
[Route("/Sessions", "GET", Summary = "Gets a list of sessions")]
|
||||
[Authenticated]
|
||||
public class GetSessions : IReturn<List<SessionInfoDto>>
|
||||
{
|
||||
[ApiMember(Name = "ControllableByUserId", Description = "Optional. Filter by sessions that a given user is allowed to remote control.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
@ -27,6 +30,7 @@ namespace MediaBrowser.Api
|
||||
/// Class DisplayContent
|
||||
/// </summary>
|
||||
[Route("/Sessions/{Id}/Viewing", "POST", Summary = "Instructs a session to browse to an item or view")]
|
||||
[Authenticated]
|
||||
public class DisplayContent : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
@ -59,6 +63,7 @@ namespace MediaBrowser.Api
|
||||
}
|
||||
|
||||
[Route("/Sessions/{Id}/Playing", "POST", Summary = "Instructs a session to play an item")]
|
||||
[Authenticated]
|
||||
public class Play : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
@ -91,6 +96,7 @@ namespace MediaBrowser.Api
|
||||
}
|
||||
|
||||
[Route("/Sessions/{Id}/Playing/{Command}", "POST", Summary = "Issues a playstate command to a client")]
|
||||
[Authenticated]
|
||||
public class SendPlaystateCommand : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
@ -115,6 +121,7 @@ namespace MediaBrowser.Api
|
||||
}
|
||||
|
||||
[Route("/Sessions/{Id}/System/{Command}", "POST", Summary = "Issues a system command to a client")]
|
||||
[Authenticated]
|
||||
public class SendSystemCommand : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
@ -133,6 +140,7 @@ namespace MediaBrowser.Api
|
||||
}
|
||||
|
||||
[Route("/Sessions/{Id}/Command/{Command}", "POST", Summary = "Issues a system command to a client")]
|
||||
[Authenticated]
|
||||
public class SendGeneralCommand : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
@ -151,6 +159,7 @@ namespace MediaBrowser.Api
|
||||
}
|
||||
|
||||
[Route("/Sessions/{Id}/Command", "POST", Summary = "Issues a system command to a client")]
|
||||
[Authenticated]
|
||||
public class SendFullGeneralCommand : GeneralCommand, IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
@ -162,6 +171,7 @@ namespace MediaBrowser.Api
|
||||
}
|
||||
|
||||
[Route("/Sessions/{Id}/Message", "POST", Summary = "Issues a command to a client to display a message to the user")]
|
||||
[Authenticated]
|
||||
public class SendMessageCommand : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
@ -182,6 +192,7 @@ namespace MediaBrowser.Api
|
||||
}
|
||||
|
||||
[Route("/Sessions/{Id}/Users/{UserId}", "POST", Summary = "Adds an additional user to a session")]
|
||||
[Authenticated]
|
||||
public class AddUserToSession : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
@ -192,6 +203,7 @@ namespace MediaBrowser.Api
|
||||
}
|
||||
|
||||
[Route("/Sessions/{Id}/Users/{UserId}", "DELETE", Summary = "Removes an additional user from a session")]
|
||||
[Authenticated]
|
||||
public class RemoveUserFromSession : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
@ -202,7 +214,7 @@ namespace MediaBrowser.Api
|
||||
}
|
||||
|
||||
[Route("/Sessions/Capabilities", "POST", Summary = "Updates capabilities for a device")]
|
||||
[Route("/Sessions/{Id}/Capabilities", "POST", Summary = "Updates capabilities for a device")]
|
||||
[Authenticated]
|
||||
public class PostCapabilities : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
@ -225,6 +237,30 @@ namespace MediaBrowser.Api
|
||||
public bool SupportsMediaControl { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Sessions/Logout", "POST", Summary = "Reports that a session has ended")]
|
||||
public class ReportSessionEnded : IReturnVoid
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Auth/Keys", "GET")]
|
||||
public class GetApiKeys
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Auth/Keys/{Key}", "DELETE")]
|
||||
public class RevokeKey
|
||||
{
|
||||
[ApiMember(Name = "Key", Description = "Auth Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public string Key { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Auth/Keys", "POST")]
|
||||
public class CreateKey
|
||||
{
|
||||
[ApiMember(Name = "App", Description = "App", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string App { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class SessionsService
|
||||
/// </summary>
|
||||
@ -236,16 +272,60 @@ namespace MediaBrowser.Api
|
||||
private readonly ISessionManager _sessionManager;
|
||||
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IAuthorizationContext _authContext;
|
||||
private readonly IAuthenticationRepository _authRepo;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SessionsService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="sessionManager">The session manager.</param>
|
||||
/// <param name="userManager">The user manager.</param>
|
||||
public SessionsService(ISessionManager sessionManager, IUserManager userManager)
|
||||
/// <param name="authContext">The authentication context.</param>
|
||||
/// <param name="authRepo">The authentication repo.</param>
|
||||
public SessionsService(ISessionManager sessionManager, IUserManager userManager, IAuthorizationContext authContext, IAuthenticationRepository authRepo)
|
||||
{
|
||||
_sessionManager = sessionManager;
|
||||
_userManager = userManager;
|
||||
_authContext = authContext;
|
||||
_authRepo = authRepo;
|
||||
}
|
||||
|
||||
public void Delete(RevokeKey request)
|
||||
{
|
||||
var task = _sessionManager.RevokeToken(request.Key);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
public void Post(CreateKey request)
|
||||
{
|
||||
var task = _authRepo.Create(new AuthenticationInfo
|
||||
{
|
||||
AppName = request.App,
|
||||
IsActive = true,
|
||||
AccessToken = Guid.NewGuid().ToString("N"),
|
||||
DateCreated = DateTime.UtcNow
|
||||
|
||||
}, CancellationToken.None);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
public void Post(ReportSessionEnded request)
|
||||
{
|
||||
var auth = _authContext.GetAuthorizationInfo(Request);
|
||||
|
||||
_sessionManager.Logout(auth.Token);
|
||||
}
|
||||
|
||||
public object Get(GetApiKeys request)
|
||||
{
|
||||
var result = _authRepo.Get(new AuthenticationInfoQuery
|
||||
{
|
||||
IsActive = true
|
||||
});
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -315,21 +395,24 @@ namespace MediaBrowser.Api
|
||||
public void Post(SendSystemCommand request)
|
||||
{
|
||||
GeneralCommandType commandType;
|
||||
var name = request.Command;
|
||||
|
||||
if (Enum.TryParse(request.Command, true, out commandType))
|
||||
if (Enum.TryParse(name, true, out commandType))
|
||||
{
|
||||
var currentSession = GetSession();
|
||||
|
||||
var command = new GeneralCommand
|
||||
{
|
||||
Name = commandType.ToString(),
|
||||
ControllingUserId = currentSession.UserId.HasValue ? currentSession.UserId.Value.ToString("N") : null
|
||||
};
|
||||
|
||||
var task = _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, command, CancellationToken.None);
|
||||
|
||||
Task.WaitAll(task);
|
||||
name = commandType.ToString();
|
||||
}
|
||||
|
||||
var currentSession = GetSession();
|
||||
|
||||
var command = new GeneralCommand
|
||||
{
|
||||
Name = name,
|
||||
ControllingUserId = currentSession.UserId.HasValue ? currentSession.UserId.Value.ToString("N") : null
|
||||
};
|
||||
|
||||
var task = _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, command, CancellationToken.None);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -422,14 +505,5 @@ namespace MediaBrowser.Api
|
||||
MessageCallbackUrl = request.MessageCallbackUrl
|
||||
});
|
||||
}
|
||||
|
||||
private SessionInfo GetSession()
|
||||
{
|
||||
var auth = AuthorizationRequestFilterAttribute.GetAuthorization(Request);
|
||||
|
||||
return _sessionManager.Sessions.First(i => string.Equals(i.DeviceId, auth.DeviceId) &&
|
||||
string.Equals(i.Client, auth.Client) &&
|
||||
string.Equals(i.ApplicationVersion, auth.Version));
|
||||
}
|
||||
}
|
||||
}
|
@ -78,8 +78,8 @@ namespace MediaBrowser.Api
|
||||
var fields = request.GetItemFields().ToList();
|
||||
|
||||
var inputItems = user == null
|
||||
? libraryManager.RootFolder.GetRecursiveChildren(i => i.Id != item.Id)
|
||||
: user.RootFolder.GetRecursiveChildren(user, i => i.Id != item.Id);
|
||||
? libraryManager.RootFolder.GetRecursiveChildren().Where(i => i.Id != item.Id)
|
||||
: user.RootFolder.GetRecursiveChildren(user).Where(i => i.Id != item.Id);
|
||||
|
||||
var items = GetSimilaritems(item, inputItems.Where(includeInSearch), getSimilarityScore)
|
||||
.ToList();
|
||||
|
@ -1,6 +1,7 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Subtitles;
|
||||
using MediaBrowser.Model.Entities;
|
||||
@ -8,14 +9,70 @@ using MediaBrowser.Model.Providers;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Api.Library
|
||||
namespace MediaBrowser.Api.Subtitles
|
||||
{
|
||||
[Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format (vtt).")]
|
||||
[Route("/Videos/{Id}/Subtitles/{Index}", "DELETE", Summary = "Deletes an external subtitle file")]
|
||||
[Authenticated]
|
||||
public class DeleteSubtitle
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "DELETE")]
|
||||
public int Index { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Items/{Id}/RemoteSearch/Subtitles/{Language}", "GET")]
|
||||
[Authenticated]
|
||||
public class SearchRemoteSubtitles : IReturn<List<RemoteSubtitleInfo>>
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[ApiMember(Name = "Language", Description = "Language", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Language { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Items/{Id}/RemoteSearch/Subtitles/Providers", "GET")]
|
||||
[Authenticated]
|
||||
public class GetSubtitleProviders : IReturn<List<SubtitleProviderInfo>>
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Items/{Id}/RemoteSearch/Subtitles/{SubtitleId}", "POST")]
|
||||
[Authenticated]
|
||||
public class DownloadRemoteSubtitles : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[ApiMember(Name = "SubtitleId", Description = "SubtitleId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public string SubtitleId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Providers/Subtitles/Subtitles/{Id}", "GET")]
|
||||
[Authenticated]
|
||||
public class GetRemoteSubtitles : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format.")]
|
||||
[Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/{StartPositionTicks}/Stream.{Format}", "GET", Summary = "Gets subtitles in a specified format.")]
|
||||
public class GetSubtitle
|
||||
{
|
||||
/// <summary>
|
||||
@ -36,54 +93,29 @@ namespace MediaBrowser.Api.Library
|
||||
|
||||
[ApiMember(Name = "StartPositionTicks", Description = "StartPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public long StartPositionTicks { get; set; }
|
||||
|
||||
[ApiMember(Name = "EndPositionTicks", Description = "EndPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public long? EndPositionTicks { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Videos/{Id}/Subtitles/{Index}", "DELETE", Summary = "Deletes an external subtitle file")]
|
||||
public class DeleteSubtitle
|
||||
[Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/subtitles.m3u8", "GET", Summary = "Gets an HLS subtitle playlist.")]
|
||||
public class GetSubtitlePlaylist
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "DELETE")]
|
||||
[ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string MediaSourceId { get; set; }
|
||||
|
||||
[ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
|
||||
public int Index { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Items/{Id}/RemoteSearch/Subtitles/{Language}", "GET")]
|
||||
public class SearchRemoteSubtitles : IReturn<List<RemoteSubtitleInfo>>
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[ApiMember(Name = "Language", Description = "Language", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Language { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Items/{Id}/RemoteSearch/Subtitles/Providers", "GET")]
|
||||
public class GetSubtitleProviders : IReturn<List<SubtitleProviderInfo>>
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Items/{Id}/RemoteSearch/Subtitles/{SubtitleId}", "POST")]
|
||||
public class DownloadRemoteSubtitles : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[ApiMember(Name = "SubtitleId", Description = "SubtitleId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public string SubtitleId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Providers/Subtitles/Subtitles/{Id}", "GET")]
|
||||
public class GetRemoteSubtitles : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Id { get; set; }
|
||||
[ApiMember(Name = "SegmentLength", Description = "The subtitle srgment length", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
|
||||
public int SegmentLength { get; set; }
|
||||
}
|
||||
|
||||
public class SubtitleService : BaseApiService
|
||||
@ -99,16 +131,59 @@ namespace MediaBrowser.Api.Library
|
||||
_subtitleEncoder = subtitleEncoder;
|
||||
}
|
||||
|
||||
public object Get(SearchRemoteSubtitles request)
|
||||
public object Get(GetSubtitlePlaylist request)
|
||||
{
|
||||
var video = (Video)_libraryManager.GetItemById(request.Id);
|
||||
var item = (Video)_libraryManager.GetItemById(new Guid(request.Id));
|
||||
|
||||
var response = _subtitleManager.SearchSubtitles(video, request.Language, CancellationToken.None).Result;
|
||||
var mediaSource = item.GetMediaSources(false)
|
||||
.First(i => string.Equals(i.Id, request.MediaSourceId ?? request.Id));
|
||||
|
||||
return ToOptimizedResult(response);
|
||||
var builder = new StringBuilder();
|
||||
|
||||
var runtime = mediaSource.RunTimeTicks ?? -1;
|
||||
|
||||
if (runtime <= 0)
|
||||
{
|
||||
throw new ArgumentException("HLS Subtitles are not supported for this media.");
|
||||
}
|
||||
|
||||
builder.AppendLine("#EXTM3U");
|
||||
builder.AppendLine("#EXT-X-TARGETDURATION:" + request.SegmentLength.ToString(CultureInfo.InvariantCulture));
|
||||
builder.AppendLine("#EXT-X-VERSION:3");
|
||||
builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
|
||||
|
||||
long positionTicks = 0;
|
||||
var segmentLengthTicks = TimeSpan.FromSeconds(request.SegmentLength).Ticks;
|
||||
|
||||
while (positionTicks < runtime)
|
||||
{
|
||||
var remaining = runtime - positionTicks;
|
||||
var lengthTicks = Math.Min(remaining, segmentLengthTicks);
|
||||
|
||||
builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
|
||||
|
||||
var url = string.Format("stream.srt?StartPositionTicks={0}&EndPositionTicks={1}",
|
||||
positionTicks.ToString(CultureInfo.InvariantCulture),
|
||||
endPositionTicks.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
builder.AppendLine(url);
|
||||
|
||||
positionTicks += segmentLengthTicks;
|
||||
}
|
||||
|
||||
builder.AppendLine("#EXT-X-ENDLIST");
|
||||
|
||||
return ResultFactory.GetResult(builder.ToString(), Common.Net.MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
|
||||
}
|
||||
|
||||
public object Get(GetSubtitle request)
|
||||
{
|
||||
if (string.Equals(request.Format, "js", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
request.Format = "json";
|
||||
}
|
||||
if (string.IsNullOrEmpty(request.Format))
|
||||
{
|
||||
var item = (Video)_libraryManager.GetItemById(new Guid(request.Id));
|
||||
@ -134,9 +209,19 @@ namespace MediaBrowser.Api.Library
|
||||
request.Index,
|
||||
request.Format,
|
||||
request.StartPositionTicks,
|
||||
request.EndPositionTicks,
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public object Get(SearchRemoteSubtitles request)
|
||||
{
|
||||
var video = (Video)_libraryManager.GetItemById(request.Id);
|
||||
|
||||
var response = _subtitleManager.SearchSubtitles(video, request.Language, CancellationToken.None).Result;
|
||||
|
||||
return ToOptimizedResult(response);
|
||||
}
|
||||
|
||||
public void Delete(DeleteSubtitle request)
|
||||
{
|
||||
var task = _subtitleManager.DeleteSubtitles(request.Id, request.Index);
|
104
MediaBrowser.Api/Sync/SyncService.cs
Normal file
104
MediaBrowser.Api/Sync/SyncService.cs
Normal file
@ -0,0 +1,104 @@
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Sync;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Sync;
|
||||
using ServiceStack;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Api.Sync
|
||||
{
|
||||
[Route("/Sync/Jobs/{Id}", "DELETE", Summary = "Cancels a sync job.")]
|
||||
public class CancelSyncJob : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Sync/Jobs/{Id}", "GET", Summary = "Gets a sync job.")]
|
||||
public class GetSyncJob : IReturn<SyncJob>
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Sync/Jobs", "GET", Summary = "Gets sync jobs.")]
|
||||
public class GetSyncJobs : IReturn<QueryResult<SyncJob>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Skips over a given number of items within the results. Use for paging.
|
||||
/// </summary>
|
||||
/// <value>The start index.</value>
|
||||
[ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? StartIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of items to return
|
||||
/// </summary>
|
||||
/// <value>The limit.</value>
|
||||
[ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? Limit { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Sync/Jobs", "POST", Summary = "Gets sync jobs.")]
|
||||
public class CreateSyncJob : SyncJobRequest, IReturn<SyncJobCreationResult>
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Sync/Targets", "GET", Summary = "Gets a list of available sync targets.")]
|
||||
public class GetSyncTarget : IReturn<List<SyncTarget>>
|
||||
{
|
||||
[ApiMember(Name = "UserId", Description = "UserId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string UserId { get; set; }
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class SyncService : BaseApiService
|
||||
{
|
||||
private readonly ISyncManager _syncManager;
|
||||
|
||||
public SyncService(ISyncManager syncManager)
|
||||
{
|
||||
_syncManager = syncManager;
|
||||
}
|
||||
|
||||
public object Get(GetSyncTarget request)
|
||||
{
|
||||
var result = _syncManager.GetSyncTargets(request.UserId);
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public object Get(GetSyncJobs request)
|
||||
{
|
||||
var result = _syncManager.GetJobs(new SyncJobQuery
|
||||
{
|
||||
StartIndex = request.StartIndex,
|
||||
Limit = request.Limit
|
||||
});
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public object Get(GetSyncJob request)
|
||||
{
|
||||
var result = _syncManager.GetJob(request.Id);
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public void Delete(CancelSyncJob request)
|
||||
{
|
||||
var task = _syncManager.CancelJob(request.Id);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
public async Task<object> Post(CreateSyncJob request)
|
||||
{
|
||||
var result = await _syncManager.CreateJob(request).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
}
|
||||
}
|
53
MediaBrowser.Api/System/ActivityLogService.cs
Normal file
53
MediaBrowser.Api/System/ActivityLogService.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using MediaBrowser.Controller.Activity;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Activity;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MediaBrowser.Api.System
|
||||
{
|
||||
[Route("/System/ActivityLog/Entries", "GET", Summary = "Gets activity log entries")]
|
||||
public class GetActivityLogs : IReturn<QueryResult<ActivityLogEntry>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Skips over a given number of items within the results. Use for paging.
|
||||
/// </summary>
|
||||
/// <value>The start index.</value>
|
||||
[ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? StartIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of items to return
|
||||
/// </summary>
|
||||
/// <value>The limit.</value>
|
||||
[ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? Limit { get; set; }
|
||||
|
||||
[ApiMember(Name = "MinDate", Description = "Optional. The minimum date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string MinDate { get; set; }
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class ActivityLogService : BaseApiService
|
||||
{
|
||||
private readonly IActivityManager _activityManager;
|
||||
|
||||
public ActivityLogService(IActivityManager activityManager)
|
||||
{
|
||||
_activityManager = activityManager;
|
||||
}
|
||||
|
||||
public object Get(GetActivityLogs request)
|
||||
{
|
||||
DateTime? minDate = string.IsNullOrWhiteSpace(request.MinDate) ?
|
||||
(DateTime?)null :
|
||||
DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
|
||||
|
||||
var result = _activityManager.GetActivityLogEntries(minDate, request.StartIndex, request.Limit);
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
}
|
||||
}
|
67
MediaBrowser.Api/System/ActivityLogWebSocketListener.cs
Normal file
67
MediaBrowser.Api/System/ActivityLogWebSocketListener.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Activity;
|
||||
using MediaBrowser.Model.Activity;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Api.System
|
||||
{
|
||||
/// <summary>
|
||||
/// Class SessionInfoWebSocketListener
|
||||
/// </summary>
|
||||
class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<List<ActivityLogEntry>, WebSocketListenerState>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
protected override string Name
|
||||
{
|
||||
get { return "ActivityLogEntry"; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The _kernel
|
||||
/// </summary>
|
||||
private readonly IActivityManager _activityManager;
|
||||
|
||||
public ActivityLogWebSocketListener(ILogger logger, IActivityManager activityManager)
|
||||
: base(logger)
|
||||
{
|
||||
_activityManager = activityManager;
|
||||
_activityManager.EntryCreated += _activityManager_EntryCreated;
|
||||
}
|
||||
|
||||
void _activityManager_EntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
|
||||
{
|
||||
SendData(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data to send.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <returns>Task{SystemInfo}.</returns>
|
||||
protected override Task<List<ActivityLogEntry>> GetDataToSend(WebSocketListenerState state)
|
||||
{
|
||||
return Task.FromResult(new List<ActivityLogEntry>());
|
||||
}
|
||||
|
||||
protected override bool SendOnTimer
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool dispose)
|
||||
{
|
||||
_activityManager.EntryCreated -= _activityManager_EntryCreated;
|
||||
|
||||
base.Dispose(dispose);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Api.WebSocket
|
||||
namespace MediaBrowser.Api.System
|
||||
{
|
||||
/// <summary>
|
||||
/// Class SystemInfoWebSocketListener
|
205
MediaBrowser.Api/System/SystemService.cs
Normal file
205
MediaBrowser.Api/System/SystemService.cs
Normal file
@ -0,0 +1,205 @@
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.System;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Api.System
|
||||
{
|
||||
/// <summary>
|
||||
/// Class GetSystemInfo
|
||||
/// </summary>
|
||||
[Route("/System/Info", "GET", Summary = "Gets information about the server")]
|
||||
[Authenticated]
|
||||
public class GetSystemInfo : IReturn<SystemInfo>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[Route("/System/Info/Public", "GET", Summary = "Gets public information about the server")]
|
||||
public class GetPublicSystemInfo : IReturn<PublicSystemInfo>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class RestartApplication
|
||||
/// </summary>
|
||||
[Route("/System/Restart", "POST", Summary = "Restarts the application, if needed")]
|
||||
[Authenticated]
|
||||
public class RestartApplication
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is currently not authenticated because the uninstaller needs to be able to shutdown the server.
|
||||
/// </summary>
|
||||
[Route("/System/Shutdown", "POST", Summary = "Shuts down the application")]
|
||||
public class ShutdownApplication
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/System/Logs", "GET", Summary = "Gets a list of available server log files")]
|
||||
[Authenticated]
|
||||
public class GetServerLogs : IReturn<List<LogFile>>
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/System/Endpoint", "GET", Summary = "Gets information about the request endpoint")]
|
||||
[Authenticated]
|
||||
public class GetEndpointInfo : IReturn<EndpointInfo>
|
||||
{
|
||||
public string Endpoint { get; set; }
|
||||
}
|
||||
|
||||
[Route("/System/Logs/Log", "GET", Summary = "Gets a log file")]
|
||||
public class GetLogFile
|
||||
{
|
||||
[ApiMember(Name = "Name", Description = "The log file name.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class SystemInfoService
|
||||
/// </summary>
|
||||
public class SystemService : BaseApiService
|
||||
{
|
||||
/// <summary>
|
||||
/// The _app host
|
||||
/// </summary>
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
private readonly INetworkManager _network;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SystemService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="appHost">The app host.</param>
|
||||
/// <param name="appPaths">The application paths.</param>
|
||||
/// <param name="fileSystem">The file system.</param>
|
||||
/// <exception cref="ArgumentNullException">jsonSerializer</exception>
|
||||
public SystemService(IServerApplicationHost appHost, IApplicationPaths appPaths, IFileSystem fileSystem, INetworkManager network)
|
||||
{
|
||||
_appHost = appHost;
|
||||
_appPaths = appPaths;
|
||||
_fileSystem = fileSystem;
|
||||
_network = network;
|
||||
}
|
||||
|
||||
public object Get(GetServerLogs request)
|
||||
{
|
||||
List<FileInfo> files;
|
||||
|
||||
try
|
||||
{
|
||||
files = new DirectoryInfo(_appPaths.LogDirectoryPath)
|
||||
.EnumerateFiles("*", SearchOption.AllDirectories)
|
||||
.Where(i => string.Equals(i.Extension, ".txt", global::System.StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
files = new List<FileInfo>();
|
||||
}
|
||||
|
||||
var result = files.Select(i => new LogFile
|
||||
{
|
||||
DateCreated = _fileSystem.GetCreationTimeUtc(i),
|
||||
DateModified = _fileSystem.GetLastWriteTimeUtc(i),
|
||||
Name = i.Name,
|
||||
Size = i.Length
|
||||
|
||||
}).OrderByDescending(i => i.DateModified)
|
||||
.ThenByDescending(i => i.DateCreated)
|
||||
.ThenBy(i => i.Name)
|
||||
.ToList();
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public object Get(GetLogFile request)
|
||||
{
|
||||
var file = new DirectoryInfo(_appPaths.LogDirectoryPath)
|
||||
.EnumerateFiles("*", SearchOption.AllDirectories)
|
||||
.First(i => string.Equals(i.Name, request.Name, global::System.StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShare.ReadWrite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
public object Get(GetSystemInfo request)
|
||||
{
|
||||
var result = _appHost.GetSystemInfo();
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public object Get(GetPublicSystemInfo request)
|
||||
{
|
||||
var result = _appHost.GetSystemInfo();
|
||||
|
||||
var publicInfo = new PublicSystemInfo
|
||||
{
|
||||
Id = result.Id,
|
||||
ServerName = result.ServerName,
|
||||
Version = result.Version
|
||||
};
|
||||
|
||||
return ToOptimizedResult(publicInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public void Post(RestartApplication request)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
await _appHost.Restart().ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public void Post(ShutdownApplication request)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
await _appHost.Shutdown().ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
|
||||
public object Get(GetEndpointInfo request)
|
||||
{
|
||||
return ToOptimizedResult(new EndpointInfo
|
||||
{
|
||||
IsLocal = Request.IsLocal,
|
||||
IsInNetwork = _network.IsInLocalNetwork(request.Endpoint ?? Request.RemoteIp)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class EndpointInfo
|
||||
{
|
||||
public bool IsLocal { get; set; }
|
||||
public bool IsInNetwork { get; set; }
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Model.System;
|
||||
using ServiceStack;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Api
|
||||
{
|
||||
/// <summary>
|
||||
/// Class GetSystemInfo
|
||||
/// </summary>
|
||||
[Route("/System/Info", "GET", Summary = "Gets information about the server")]
|
||||
public class GetSystemInfo : IReturn<SystemInfo>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class RestartApplication
|
||||
/// </summary>
|
||||
[Route("/System/Restart", "POST", Summary = "Restarts the application, if needed")]
|
||||
public class RestartApplication
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/System/Shutdown", "POST", Summary = "Shuts down the application")]
|
||||
public class ShutdownApplication
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class SystemInfoService
|
||||
/// </summary>
|
||||
public class SystemService : BaseApiService
|
||||
{
|
||||
/// <summary>
|
||||
/// The _app host
|
||||
/// </summary>
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SystemService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="appHost">The app host.</param>
|
||||
/// <exception cref="System.ArgumentNullException">jsonSerializer</exception>
|
||||
public SystemService(IServerApplicationHost appHost)
|
||||
{
|
||||
_appHost = appHost;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
public object Get(GetSystemInfo request)
|
||||
{
|
||||
var result = _appHost.GetSystemInfo();
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public void Post(RestartApplication request)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
await _appHost.Restart().ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public void Post(ShutdownApplication request)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
await _appHost.Shutdown().ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
@ -175,6 +176,7 @@ namespace MediaBrowser.Api
|
||||
/// <summary>
|
||||
/// Class TvShowsService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class TvShowsService : BaseApiService
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -2,6 +2,7 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Querying;
|
||||
@ -46,6 +47,7 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
/// <summary>
|
||||
/// Class ArtistsService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class ArtistsService : BaseItemsByNameService<MusicArtist>
|
||||
{
|
||||
/// <summary>
|
||||
@ -88,10 +90,10 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
{
|
||||
var user = UserManager.GetUserById(request.UserId.Value);
|
||||
|
||||
return DtoService.GetItemByNameDto(item, fields.ToList(), user);
|
||||
return DtoService.GetBaseItemDto(item, fields.ToList(), user);
|
||||
}
|
||||
|
||||
return DtoService.GetItemByNameDto(item, fields.ToList());
|
||||
return DtoService.GetBaseItemDto(item, fields.ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,9 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Api.UserLibrary
|
||||
{
|
||||
|
@ -1,6 +1,7 @@
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
@ -41,6 +42,7 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
public Guid? UserId { get; set; }
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class GameGenresService : BaseItemsByNameService<GameGenre>
|
||||
{
|
||||
public GameGenresService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, IItemRepository itemRepo, IDtoService dtoService)
|
||||
@ -76,10 +78,10 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
{
|
||||
var user = UserManager.GetUserById(request.UserId.Value);
|
||||
|
||||
return DtoService.GetItemByNameDto(item, fields.ToList(), user);
|
||||
return DtoService.GetBaseItemDto(item, fields.ToList(), user);
|
||||
}
|
||||
|
||||
return DtoService.GetItemByNameDto(item, fields.ToList());
|
||||
return DtoService.GetBaseItemDto(item, fields.ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -2,6 +2,7 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Querying;
|
||||
@ -46,6 +47,7 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
/// <summary>
|
||||
/// Class GenresService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class GenresService : BaseItemsByNameService<Genre>
|
||||
{
|
||||
public GenresService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, IItemRepository itemRepo, IDtoService dtoService)
|
||||
@ -81,10 +83,10 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
{
|
||||
var user = UserManager.GetUserById(request.UserId.Value);
|
||||
|
||||
return DtoService.GetItemByNameDto(item, fields.ToList(), user);
|
||||
return DtoService.GetBaseItemDto(item, fields.ToList(), user);
|
||||
}
|
||||
|
||||
return DtoService.GetItemByNameDto(item, fields.ToList());
|
||||
return DtoService.GetBaseItemDto(item, fields.ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -6,6 +6,7 @@ using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Localization;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
@ -246,6 +247,7 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
/// <summary>
|
||||
/// Class ItemsService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class ItemsService : BaseApiService
|
||||
{
|
||||
/// <summary>
|
||||
@ -1428,7 +1430,7 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
nextId = list[index + 1].Id;
|
||||
}
|
||||
|
||||
return list.Where(i => i.Id == previousId || i.Id == nextId);
|
||||
return list.Where(i => i.Id == previousId || i.Id == nextId || i.Id == adjacentToIdGuid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -2,6 +2,7 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Querying;
|
||||
@ -41,6 +42,7 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
public Guid? UserId { get; set; }
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class MusicGenresService : BaseItemsByNameService<MusicGenre>
|
||||
{
|
||||
public MusicGenresService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, IItemRepository itemRepo, IDtoService dtoService)
|
||||
@ -76,10 +78,10 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
{
|
||||
var user = UserManager.GetUserById(request.UserId.Value);
|
||||
|
||||
return DtoService.GetItemByNameDto(item, fields.ToList(), user);
|
||||
return DtoService.GetBaseItemDto(item, fields.ToList(), user);
|
||||
}
|
||||
|
||||
return DtoService.GetItemByNameDto(item, fields.ToList());
|
||||
return DtoService.GetBaseItemDto(item, fields.ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,6 +1,7 @@
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Querying;
|
||||
@ -51,6 +52,7 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
/// <summary>
|
||||
/// Class PersonsService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class PersonsService : BaseItemsByNameService<Person>
|
||||
{
|
||||
/// <summary>
|
||||
@ -93,10 +95,10 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
{
|
||||
var user = UserManager.GetUserById(request.UserId.Value);
|
||||
|
||||
return DtoService.GetItemByNameDto(item, fields.ToList(), user);
|
||||
return DtoService.GetBaseItemDto(item, fields.ToList(), user);
|
||||
}
|
||||
|
||||
return DtoService.GetItemByNameDto(item, fields.ToList());
|
||||
return DtoService.GetBaseItemDto(item, fields.ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
388
MediaBrowser.Api/UserLibrary/PlaystateService.cs
Normal file
388
MediaBrowser.Api/UserLibrary/PlaystateService.cs
Normal file
@ -0,0 +1,388 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Session;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Api.UserLibrary
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MarkPlayedItem
|
||||
/// </summary>
|
||||
[Route("/Users/{UserId}/PlayedItems/{Id}", "POST")]
|
||||
[Api(Description = "Marks an item as played")]
|
||||
public class MarkPlayedItem : IReturn<UserItemDataDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
/// <value>The user id.</value>
|
||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
[ApiMember(Name = "DatePlayed", Description = "The date the item was played (if any). Format = yyyyMMddHHmmss", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string DatePlayed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class MarkUnplayedItem
|
||||
/// </summary>
|
||||
[Route("/Users/{UserId}/PlayedItems/{Id}", "DELETE")]
|
||||
[Api(Description = "Marks an item as unplayed")]
|
||||
public class MarkUnplayedItem : IReturn<UserItemDataDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
/// <value>The user id.</value>
|
||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Sessions/Playing", "POST")]
|
||||
[Api(Description = "Reports playback has started within a session")]
|
||||
public class ReportPlaybackStart : PlaybackStartInfo, IReturnVoid
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Sessions/Playing/Progress", "POST")]
|
||||
[Api(Description = "Reports playback progress within a session")]
|
||||
public class ReportPlaybackProgress : PlaybackProgressInfo, IReturnVoid
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Sessions/Playing/Stopped", "POST")]
|
||||
[Api(Description = "Reports playback has stopped within a session")]
|
||||
public class ReportPlaybackStopped : PlaybackStopInfo, IReturnVoid
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class OnPlaybackStart
|
||||
/// </summary>
|
||||
[Route("/Users/{UserId}/PlayingItems/{Id}", "POST")]
|
||||
[Api(Description = "Reports that a user has begun playing an item")]
|
||||
public class OnPlaybackStart : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
/// <value>The user id.</value>
|
||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string MediaSourceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this <see cref="UpdateUserItemRating" /> is likes.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if likes; otherwise, <c>false</c>.</value>
|
||||
[ApiMember(Name = "CanSeek", Description = "Indicates if the client can seek", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
|
||||
public bool CanSeek { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "QueueableMediaTypes", Description = "A list of media types that can be queued from this item, comma delimited. Audio,Video,Book,Game", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
|
||||
public string QueueableMediaTypes { get; set; }
|
||||
|
||||
[ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
||||
public int? AudioStreamIndex { get; set; }
|
||||
|
||||
[ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
||||
public int? SubtitleStreamIndex { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class OnPlaybackProgress
|
||||
/// </summary>
|
||||
[Route("/Users/{UserId}/PlayingItems/{Id}/Progress", "POST")]
|
||||
[Api(Description = "Reports a user's playback progress")]
|
||||
public class OnPlaybackProgress : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
/// <value>The user id.</value>
|
||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string MediaSourceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the position ticks.
|
||||
/// </summary>
|
||||
/// <value>The position ticks.</value>
|
||||
[ApiMember(Name = "PositionTicks", Description = "Optional. The current position, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
||||
public long? PositionTicks { get; set; }
|
||||
|
||||
[ApiMember(Name = "IsPaused", Description = "Indicates if the player is paused.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
|
||||
public bool IsPaused { get; set; }
|
||||
|
||||
[ApiMember(Name = "IsMuted", Description = "Indicates if the player is muted.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
|
||||
public bool IsMuted { get; set; }
|
||||
|
||||
[ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
||||
public int? AudioStreamIndex { get; set; }
|
||||
|
||||
[ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
||||
public int? SubtitleStreamIndex { get; set; }
|
||||
|
||||
[ApiMember(Name = "VolumeLevel", Description = "Scale of 0-100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
||||
public int? VolumeLevel { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class OnPlaybackStopped
|
||||
/// </summary>
|
||||
[Route("/Users/{UserId}/PlayingItems/{Id}", "DELETE")]
|
||||
[Api(Description = "Reports that a user has stopped playing an item")]
|
||||
public class OnPlaybackStopped : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
/// <value>The user id.</value>
|
||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
|
||||
public string MediaSourceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the position ticks.
|
||||
/// </summary>
|
||||
/// <value>The position ticks.</value>
|
||||
[ApiMember(Name = "PositionTicks", Description = "Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "DELETE")]
|
||||
public long? PositionTicks { get; set; }
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class PlaystateService : BaseApiService
|
||||
{
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IUserDataManager _userDataRepository;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
|
||||
public PlaystateService(IUserManager userManager, IUserDataManager userDataRepository, ILibraryManager libraryManager, ISessionManager sessionManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_userDataRepository = userDataRepository;
|
||||
_libraryManager = libraryManager;
|
||||
_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public object Post(MarkPlayedItem request)
|
||||
{
|
||||
var result = MarkPlayed(request).Result;
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
private async Task<UserItemDataDto> MarkPlayed(MarkPlayedItem request)
|
||||
{
|
||||
var user = _userManager.GetUserById(request.UserId);
|
||||
|
||||
DateTime? datePlayed = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(request.DatePlayed))
|
||||
{
|
||||
datePlayed = DateTime.ParseExact(request.DatePlayed, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
|
||||
}
|
||||
|
||||
var session = GetSession();
|
||||
|
||||
var dto = await UpdatePlayedStatus(user, request.Id, true, datePlayed).ConfigureAwait(false);
|
||||
|
||||
foreach (var additionalUserInfo in session.AdditionalUsers)
|
||||
{
|
||||
var additionalUser = _userManager.GetUserById(new Guid(additionalUserInfo.UserId));
|
||||
|
||||
await UpdatePlayedStatus(additionalUser, request.Id, true, datePlayed).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public void Post(OnPlaybackStart request)
|
||||
{
|
||||
var queueableMediaTypes = (request.QueueableMediaTypes ?? string.Empty);
|
||||
|
||||
Post(new ReportPlaybackStart
|
||||
{
|
||||
CanSeek = request.CanSeek,
|
||||
ItemId = request.Id,
|
||||
QueueableMediaTypes = queueableMediaTypes.Split(',').ToList(),
|
||||
MediaSourceId = request.MediaSourceId,
|
||||
AudioStreamIndex = request.AudioStreamIndex,
|
||||
SubtitleStreamIndex = request.SubtitleStreamIndex
|
||||
});
|
||||
}
|
||||
|
||||
public void Post(ReportPlaybackStart request)
|
||||
{
|
||||
request.SessionId = GetSession().Id;
|
||||
|
||||
var task = _sessionManager.OnPlaybackStart(request);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public void Post(OnPlaybackProgress request)
|
||||
{
|
||||
Post(new ReportPlaybackProgress
|
||||
{
|
||||
ItemId = request.Id,
|
||||
PositionTicks = request.PositionTicks,
|
||||
IsMuted = request.IsMuted,
|
||||
IsPaused = request.IsPaused,
|
||||
MediaSourceId = request.MediaSourceId,
|
||||
AudioStreamIndex = request.AudioStreamIndex,
|
||||
SubtitleStreamIndex = request.SubtitleStreamIndex,
|
||||
VolumeLevel = request.VolumeLevel
|
||||
});
|
||||
}
|
||||
|
||||
public void Post(ReportPlaybackProgress request)
|
||||
{
|
||||
request.SessionId = GetSession().Id;
|
||||
|
||||
var task = _sessionManager.OnPlaybackProgress(request);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public void Delete(OnPlaybackStopped request)
|
||||
{
|
||||
Post(new ReportPlaybackStopped
|
||||
{
|
||||
ItemId = request.Id,
|
||||
PositionTicks = request.PositionTicks,
|
||||
MediaSourceId = request.MediaSourceId
|
||||
});
|
||||
}
|
||||
|
||||
public void Post(ReportPlaybackStopped request)
|
||||
{
|
||||
request.SessionId = GetSession().Id;
|
||||
|
||||
var task = _sessionManager.OnPlaybackStopped(request);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public object Delete(MarkUnplayedItem request)
|
||||
{
|
||||
var task = MarkUnplayed(request);
|
||||
|
||||
return ToOptimizedResult(task.Result);
|
||||
}
|
||||
|
||||
private async Task<UserItemDataDto> MarkUnplayed(MarkUnplayedItem request)
|
||||
{
|
||||
var user = _userManager.GetUserById(request.UserId);
|
||||
|
||||
var session = GetSession();
|
||||
|
||||
var dto = await UpdatePlayedStatus(user, request.Id, false, null).ConfigureAwait(false);
|
||||
|
||||
foreach (var additionalUserInfo in session.AdditionalUsers)
|
||||
{
|
||||
var additionalUser = _userManager.GetUserById(new Guid(additionalUserInfo.UserId));
|
||||
|
||||
await UpdatePlayedStatus(additionalUser, request.Id, false, null).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the played status.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
|
||||
/// <param name="datePlayed">The date played.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task<UserItemDataDto> UpdatePlayedStatus(User user, string itemId, bool wasPlayed, DateTime? datePlayed)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
|
||||
if (wasPlayed)
|
||||
{
|
||||
await item.MarkPlayed(user, datePlayed, _userDataRepository).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await item.MarkUnplayed(user, _userDataRepository).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return _userDataRepository.GetUserDataDto(item, user);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Querying;
|
||||
@ -43,6 +44,7 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
/// <summary>
|
||||
/// Class StudiosService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class StudiosService : BaseItemsByNameService<Studio>
|
||||
{
|
||||
public StudiosService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, IItemRepository itemRepo, IDtoService dtoService)
|
||||
@ -78,10 +80,10 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
{
|
||||
var user = UserManager.GetUserById(request.UserId.Value);
|
||||
|
||||
return DtoService.GetItemByNameDto(item, fields.ToList(), user);
|
||||
return DtoService.GetBaseItemDto(item, fields.ToList(), user);
|
||||
}
|
||||
|
||||
return DtoService.GetItemByNameDto(item, fields.ToList());
|
||||
return DtoService.GetBaseItemDto(item, fields.ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -3,16 +3,14 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Library;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Session;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -187,195 +185,6 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
public bool Likes { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class MarkPlayedItem
|
||||
/// </summary>
|
||||
[Route("/Users/{UserId}/PlayedItems/{Id}", "POST")]
|
||||
[Api(Description = "Marks an item as played")]
|
||||
public class MarkPlayedItem : IReturn<UserItemDataDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
/// <value>The user id.</value>
|
||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
[ApiMember(Name = "DatePlayed", Description = "The date the item was played (if any). Format = yyyyMMddHHmmss", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string DatePlayed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class MarkUnplayedItem
|
||||
/// </summary>
|
||||
[Route("/Users/{UserId}/PlayedItems/{Id}", "DELETE")]
|
||||
[Api(Description = "Marks an item as unplayed")]
|
||||
public class MarkUnplayedItem : IReturn<UserItemDataDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
/// <value>The user id.</value>
|
||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Sessions/Playing", "POST")]
|
||||
[Api(Description = "Reports playback has started within a session")]
|
||||
public class ReportPlaybackStart : PlaybackStartInfo, IReturnVoid
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Sessions/Playing/Progress", "POST")]
|
||||
[Api(Description = "Reports playback progress within a session")]
|
||||
public class ReportPlaybackProgress : PlaybackProgressInfo, IReturnVoid
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Sessions/Playing/Stopped", "POST")]
|
||||
[Api(Description = "Reports playback has stopped within a session")]
|
||||
public class ReportPlaybackStopped : PlaybackStopInfo, IReturnVoid
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class OnPlaybackStart
|
||||
/// </summary>
|
||||
[Route("/Users/{UserId}/PlayingItems/{Id}", "POST")]
|
||||
[Api(Description = "Reports that a user has begun playing an item")]
|
||||
public class OnPlaybackStart : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
/// <value>The user id.</value>
|
||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string MediaSourceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this <see cref="UpdateUserItemRating" /> is likes.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if likes; otherwise, <c>false</c>.</value>
|
||||
[ApiMember(Name = "CanSeek", Description = "Indicates if the client can seek", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
|
||||
public bool CanSeek { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "QueueableMediaTypes", Description = "A list of media types that can be queued from this item, comma delimited. Audio,Video,Book,Game", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
|
||||
public string QueueableMediaTypes { get; set; }
|
||||
|
||||
[ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
||||
public int? AudioStreamIndex { get; set; }
|
||||
|
||||
[ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
||||
public int? SubtitleStreamIndex { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class OnPlaybackProgress
|
||||
/// </summary>
|
||||
[Route("/Users/{UserId}/PlayingItems/{Id}/Progress", "POST")]
|
||||
[Api(Description = "Reports a user's playback progress")]
|
||||
public class OnPlaybackProgress : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
/// <value>The user id.</value>
|
||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string MediaSourceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the position ticks.
|
||||
/// </summary>
|
||||
/// <value>The position ticks.</value>
|
||||
[ApiMember(Name = "PositionTicks", Description = "Optional. The current position, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
||||
public long? PositionTicks { get; set; }
|
||||
|
||||
[ApiMember(Name = "IsPaused", Description = "Indicates if the player is paused.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
|
||||
public bool IsPaused { get; set; }
|
||||
|
||||
[ApiMember(Name = "IsMuted", Description = "Indicates if the player is muted.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")]
|
||||
public bool IsMuted { get; set; }
|
||||
|
||||
[ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
||||
public int? AudioStreamIndex { get; set; }
|
||||
|
||||
[ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
||||
public int? SubtitleStreamIndex { get; set; }
|
||||
|
||||
[ApiMember(Name = "VolumeLevel", Description = "Scale of 0-100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
||||
public int? VolumeLevel { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class OnPlaybackStopped
|
||||
/// </summary>
|
||||
[Route("/Users/{UserId}/PlayingItems/{Id}", "DELETE")]
|
||||
[Api(Description = "Reports that a user has stopped playing an item")]
|
||||
public class OnPlaybackStopped : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
/// <value>The user id.</value>
|
||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
|
||||
public string MediaSourceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the position ticks.
|
||||
/// </summary>
|
||||
/// <value>The position ticks.</value>
|
||||
[ApiMember(Name = "PositionTicks", Description = "Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "DELETE")]
|
||||
public long? PositionTicks { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class GetLocalTrailers
|
||||
/// </summary>
|
||||
@ -420,28 +229,54 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Users/{UserId}/Items/Latest", "GET", Summary = "Gets latest media")]
|
||||
public class GetLatestMedia : IReturn<List<BaseItemDto>>, IHasItemFields
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
/// <value>The user id.</value>
|
||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
[ApiMember(Name = "Limit", Description = "Limit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int Limit { get; set; }
|
||||
|
||||
[ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string ParentId { get; set; }
|
||||
|
||||
[ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, CriticRatingSummary, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
|
||||
public string Fields { get; set; }
|
||||
|
||||
[ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
|
||||
public string IncludeItemTypes { get; set; }
|
||||
|
||||
[ApiMember(Name = "IsFolder", Description = "Filter by items that are folders, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
|
||||
public bool? IsFolder { get; set; }
|
||||
|
||||
[ApiMember(Name = "IsPlayed", Description = "Filter by items that are played, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
|
||||
public bool? IsPlayed { get; set; }
|
||||
|
||||
[ApiMember(Name = "GroupItems", Description = "Whether or not to group items into a parent container.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
|
||||
public bool GroupItems { get; set; }
|
||||
|
||||
public GetLatestMedia()
|
||||
{
|
||||
Limit = 20;
|
||||
GroupItems = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class UserLibraryService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class UserLibraryService : BaseApiService
|
||||
{
|
||||
/// <summary>
|
||||
/// The _user manager
|
||||
/// </summary>
|
||||
private readonly IUserManager _userManager;
|
||||
/// <summary>
|
||||
/// The _user data repository
|
||||
/// </summary>
|
||||
private readonly IUserDataManager _userDataRepository;
|
||||
/// <summary>
|
||||
/// The _library manager
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
|
||||
private readonly IUserViewManager _userViewManager;
|
||||
|
||||
/// <summary>
|
||||
@ -450,15 +285,14 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
/// <param name="userManager">The user manager.</param>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="userDataRepository">The user data repository.</param>
|
||||
/// <param name="sessionManager">The session manager.</param>
|
||||
/// <param name="dtoService">The dto service.</param>
|
||||
/// <param name="userViewManager">The user view manager.</param>
|
||||
/// <exception cref="System.ArgumentNullException">jsonSerializer</exception>
|
||||
public UserLibraryService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, ISessionManager sessionManager, IDtoService dtoService, IUserViewManager userViewManager)
|
||||
public UserLibraryService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, IDtoService dtoService, IUserViewManager userViewManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_libraryManager = libraryManager;
|
||||
_userDataRepository = userDataRepository;
|
||||
_sessionManager = sessionManager;
|
||||
_dtoService = dtoService;
|
||||
_userViewManager = userViewManager;
|
||||
}
|
||||
@ -475,7 +309,98 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
}
|
||||
|
||||
public object Get(GetUserViews request)
|
||||
public object Get(GetLatestMedia request)
|
||||
{
|
||||
var user = _userManager.GetUserById(request.UserId);
|
||||
|
||||
// Avoid implicitly captured closure
|
||||
var libraryItems = GetAllLibraryItems(request.UserId, _userManager, _libraryManager, request.ParentId)
|
||||
.OrderByDescending(i => i.DateCreated)
|
||||
.Where(i => i.LocationType != LocationType.Virtual);
|
||||
|
||||
|
||||
//if (request.IsFolder.HasValue)
|
||||
//{
|
||||
//var val = request.IsFolder.Value;
|
||||
libraryItems = libraryItems.Where(f => f.IsFolder == false);
|
||||
//}
|
||||
|
||||
if (!string.IsNullOrEmpty(request.IncludeItemTypes))
|
||||
{
|
||||
var vals = request.IncludeItemTypes.Split(',');
|
||||
libraryItems = libraryItems.Where(f => vals.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
var currentUser = user;
|
||||
|
||||
if (request.IsPlayed.HasValue)
|
||||
{
|
||||
var takeLimit = request.Limit * 20;
|
||||
|
||||
var val = request.IsPlayed.Value;
|
||||
libraryItems = libraryItems.Where(f => f.IsPlayed(currentUser) == val)
|
||||
.Take(takeLimit);
|
||||
}
|
||||
|
||||
// Avoid implicitly captured closure
|
||||
var items = libraryItems
|
||||
.ToList();
|
||||
|
||||
var list = new List<Tuple<BaseItem, List<BaseItem>>>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
// Only grab the index container for media
|
||||
var container = item.IsFolder || !request.GroupItems ? null : item.LatestItemsIndexContainer;
|
||||
|
||||
if (container == null)
|
||||
{
|
||||
list.Add(new Tuple<BaseItem, List<BaseItem>>(null, new List<BaseItem> { item }));
|
||||
}
|
||||
else
|
||||
{
|
||||
var current = list.FirstOrDefault(i => i.Item1 != null && i.Item1.Id == container.Id);
|
||||
|
||||
if (current != null)
|
||||
{
|
||||
current.Item2.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
list.Add(new Tuple<BaseItem, List<BaseItem>>(container, new List<BaseItem> { item }));
|
||||
}
|
||||
}
|
||||
|
||||
if (list.Count >= request.Limit)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var fields = request.GetItemFields().ToList();
|
||||
|
||||
var dtos = list.Select(i =>
|
||||
{
|
||||
var item = i.Item2[0];
|
||||
var childCount = 0;
|
||||
|
||||
if (i.Item1 != null && i.Item2.Count > 0)
|
||||
{
|
||||
item = i.Item1;
|
||||
childCount = i.Item2.Count;
|
||||
}
|
||||
|
||||
var dto = _dtoService.GetBaseItemDto(item, fields, user);
|
||||
|
||||
dto.ChildCount = childCount;
|
||||
|
||||
return dto;
|
||||
});
|
||||
|
||||
return ToOptimizedResult(dtos.ToList());
|
||||
}
|
||||
|
||||
public async Task<object> Get(GetUserViews request)
|
||||
{
|
||||
var user = _userManager.GetUserById(new Guid(request.UserId));
|
||||
|
||||
@ -493,10 +418,9 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
query.IncludeExternalContent = request.IncludeExternalContent.Value;
|
||||
}
|
||||
|
||||
var folders = _userViewManager.GetUserViews(query, CancellationToken.None).Result;
|
||||
var folders = await _userViewManager.GetUserViews(query, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var dtos = folders.OrderBy(i => i.SortName)
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, fields, user))
|
||||
.ToArray();
|
||||
|
||||
var result = new QueryResult<BaseItemDto>
|
||||
@ -541,7 +465,8 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
if (series != null)
|
||||
{
|
||||
var dtos = series
|
||||
.GetRecursiveChildren(i => i is Episode && i.ParentIndexNumber.HasValue && i.ParentIndexNumber.Value == 0)
|
||||
.GetRecursiveChildren()
|
||||
.Where(i => i is Episode && i.ParentIndexNumber.HasValue && i.ParentIndexNumber.Value == 0)
|
||||
.OrderBy(i =>
|
||||
{
|
||||
if (i.PremiereDate.HasValue)
|
||||
@ -714,9 +639,7 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
|
||||
await _userDataRepository.SaveUserData(user.Id, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
data = _userDataRepository.GetUserData(user.Id, key);
|
||||
|
||||
return _dtoService.GetUserItemDataDto(data);
|
||||
return _userDataRepository.GetUserDataDto(item, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -763,177 +686,7 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
|
||||
await _userDataRepository.SaveUserData(user.Id, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
data = _userDataRepository.GetUserData(user.Id, key);
|
||||
|
||||
return _dtoService.GetUserItemDataDto(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public object Post(MarkPlayedItem request)
|
||||
{
|
||||
var result = MarkPlayed(request).Result;
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
private async Task<UserItemDataDto> MarkPlayed(MarkPlayedItem request)
|
||||
{
|
||||
var user = _userManager.GetUserById(request.UserId);
|
||||
|
||||
DateTime? datePlayed = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(request.DatePlayed))
|
||||
{
|
||||
datePlayed = DateTime.ParseExact(request.DatePlayed, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
|
||||
}
|
||||
|
||||
var session = GetSession(_sessionManager);
|
||||
|
||||
var dto = await UpdatePlayedStatus(user, request.Id, true, datePlayed).ConfigureAwait(false);
|
||||
|
||||
foreach (var additionalUserInfo in session.AdditionalUsers)
|
||||
{
|
||||
var additionalUser = _userManager.GetUserById(new Guid(additionalUserInfo.UserId));
|
||||
|
||||
await UpdatePlayedStatus(additionalUser, request.Id, true, datePlayed).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public void Post(OnPlaybackStart request)
|
||||
{
|
||||
var queueableMediaTypes = (request.QueueableMediaTypes ?? string.Empty);
|
||||
|
||||
Post(new ReportPlaybackStart
|
||||
{
|
||||
CanSeek = request.CanSeek,
|
||||
ItemId = request.Id,
|
||||
QueueableMediaTypes = queueableMediaTypes.Split(',').ToList(),
|
||||
MediaSourceId = request.MediaSourceId,
|
||||
AudioStreamIndex = request.AudioStreamIndex,
|
||||
SubtitleStreamIndex = request.SubtitleStreamIndex
|
||||
});
|
||||
}
|
||||
|
||||
public void Post(ReportPlaybackStart request)
|
||||
{
|
||||
request.SessionId = GetSession(_sessionManager).Id;
|
||||
|
||||
var task = _sessionManager.OnPlaybackStart(request);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public void Post(OnPlaybackProgress request)
|
||||
{
|
||||
Post(new ReportPlaybackProgress
|
||||
{
|
||||
ItemId = request.Id,
|
||||
PositionTicks = request.PositionTicks,
|
||||
IsMuted = request.IsMuted,
|
||||
IsPaused = request.IsPaused,
|
||||
MediaSourceId = request.MediaSourceId,
|
||||
AudioStreamIndex = request.AudioStreamIndex,
|
||||
SubtitleStreamIndex = request.SubtitleStreamIndex,
|
||||
VolumeLevel = request.VolumeLevel
|
||||
});
|
||||
}
|
||||
|
||||
public void Post(ReportPlaybackProgress request)
|
||||
{
|
||||
request.SessionId = GetSession(_sessionManager).Id;
|
||||
|
||||
var task = _sessionManager.OnPlaybackProgress(request);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public void Delete(OnPlaybackStopped request)
|
||||
{
|
||||
Post(new ReportPlaybackStopped
|
||||
{
|
||||
ItemId = request.Id,
|
||||
PositionTicks = request.PositionTicks,
|
||||
MediaSourceId = request.MediaSourceId
|
||||
});
|
||||
}
|
||||
|
||||
public void Post(ReportPlaybackStopped request)
|
||||
{
|
||||
request.SessionId = GetSession(_sessionManager).Id;
|
||||
|
||||
var task = _sessionManager.OnPlaybackStopped(request);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public object Delete(MarkUnplayedItem request)
|
||||
{
|
||||
var task = MarkUnplayed(request);
|
||||
|
||||
return ToOptimizedResult(task.Result);
|
||||
}
|
||||
|
||||
private async Task<UserItemDataDto> MarkUnplayed(MarkUnplayedItem request)
|
||||
{
|
||||
var user = _userManager.GetUserById(request.UserId);
|
||||
|
||||
var session = GetSession(_sessionManager);
|
||||
|
||||
var dto = await UpdatePlayedStatus(user, request.Id, false, null).ConfigureAwait(false);
|
||||
|
||||
foreach (var additionalUserInfo in session.AdditionalUsers)
|
||||
{
|
||||
var additionalUser = _userManager.GetUserById(new Guid(additionalUserInfo.UserId));
|
||||
|
||||
await UpdatePlayedStatus(additionalUser, request.Id, false, null).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the played status.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
|
||||
/// <param name="datePlayed">The date played.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task<UserItemDataDto> UpdatePlayedStatus(User user, string itemId, bool wasPlayed, DateTime? datePlayed)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
|
||||
if (wasPlayed)
|
||||
{
|
||||
await item.MarkPlayed(user, datePlayed, _userDataRepository).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await item.MarkUnplayed(user, _userDataRepository).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return _dtoService.GetUserItemDataDto(_userDataRepository.GetUserData(user.Id, item.GetUserDataKey()));
|
||||
return _userDataRepository.GetUserDataDto(item, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Querying;
|
||||
@ -43,6 +44,7 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
/// <summary>
|
||||
/// Class YearsService
|
||||
/// </summary>
|
||||
[Authenticated]
|
||||
public class YearsService : BaseItemsByNameService<Year>
|
||||
{
|
||||
public YearsService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, IItemRepository itemRepo, IDtoService dtoService)
|
||||
@ -78,10 +80,10 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
{
|
||||
var user = UserManager.GetUserById(request.UserId.Value);
|
||||
|
||||
return DtoService.GetItemByNameDto(item, fields.ToList(), user);
|
||||
return DtoService.GetBaseItemDto(item, fields.ToList(), user);
|
||||
}
|
||||
|
||||
return DtoService.GetItemByNameDto(item, fields.ToList());
|
||||
return DtoService.GetBaseItemDto(item, fields.ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,9 +1,11 @@
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Users;
|
||||
using ServiceStack;
|
||||
using ServiceStack.Text.Controller;
|
||||
@ -18,6 +20,7 @@ namespace MediaBrowser.Api
|
||||
/// Class GetUsers
|
||||
/// </summary>
|
||||
[Route("/Users", "GET", Summary = "Gets a list of users")]
|
||||
[Authenticated]
|
||||
public class GetUsers : IReturn<List<UserDto>>
|
||||
{
|
||||
[ApiMember(Name = "IsHidden", Description = "Optional filter by IsHidden=true or false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
|
||||
@ -36,6 +39,7 @@ namespace MediaBrowser.Api
|
||||
/// Class GetUser
|
||||
/// </summary>
|
||||
[Route("/Users/{Id}", "GET", Summary = "Gets a user by Id")]
|
||||
[Authenticated]
|
||||
public class GetUser : IReturn<UserDto>
|
||||
{
|
||||
/// <summary>
|
||||
@ -50,6 +54,7 @@ namespace MediaBrowser.Api
|
||||
/// Class DeleteUser
|
||||
/// </summary>
|
||||
[Route("/Users/{Id}", "DELETE", Summary = "Deletes a user")]
|
||||
[Authenticated]
|
||||
public class DeleteUser : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
@ -106,6 +111,7 @@ namespace MediaBrowser.Api
|
||||
/// Class UpdateUserPassword
|
||||
/// </summary>
|
||||
[Route("/Users/{Id}/Password", "POST", Summary = "Updates a user's password")]
|
||||
[Authenticated]
|
||||
public class UpdateUserPassword : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
@ -137,6 +143,7 @@ namespace MediaBrowser.Api
|
||||
/// Class UpdateUser
|
||||
/// </summary>
|
||||
[Route("/Users/{Id}", "POST", Summary = "Updates a user")]
|
||||
[Authenticated]
|
||||
public class UpdateUser : UserDto, IReturnVoid
|
||||
{
|
||||
}
|
||||
@ -145,6 +152,7 @@ namespace MediaBrowser.Api
|
||||
/// Class CreateUser
|
||||
/// </summary>
|
||||
[Route("/Users", "POST", Summary = "Creates a user")]
|
||||
[Authenticated]
|
||||
public class CreateUser : UserDto, IReturn<UserDto>
|
||||
{
|
||||
}
|
||||
@ -152,48 +160,68 @@ namespace MediaBrowser.Api
|
||||
/// <summary>
|
||||
/// Class UsersService
|
||||
/// </summary>
|
||||
public class UserService : BaseApiService
|
||||
public class UserService : BaseApiService, IHasAuthorization
|
||||
{
|
||||
/// <summary>
|
||||
/// The _XML serializer
|
||||
/// </summary>
|
||||
private readonly IXmlSerializer _xmlSerializer;
|
||||
|
||||
/// <summary>
|
||||
/// The _user manager
|
||||
/// </summary>
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
private readonly ISessionManager _sessionMananger;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly INetworkManager _networkManager;
|
||||
|
||||
public IAuthorizationContext AuthorizationContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="xmlSerializer">The XML serializer.</param>
|
||||
/// <param name="userManager">The user manager.</param>
|
||||
/// <param name="dtoService">The dto service.</param>
|
||||
/// <param name="sessionMananger">The session mananger.</param>
|
||||
/// <exception cref="System.ArgumentNullException">xmlSerializer</exception>
|
||||
public UserService(IXmlSerializer xmlSerializer, IUserManager userManager, IDtoService dtoService, ISessionManager sessionMananger)
|
||||
: base()
|
||||
public UserService(IUserManager userManager, IDtoService dtoService, ISessionManager sessionMananger, IServerConfigurationManager config, INetworkManager networkManager)
|
||||
{
|
||||
if (xmlSerializer == null)
|
||||
{
|
||||
throw new ArgumentNullException("xmlSerializer");
|
||||
}
|
||||
|
||||
_xmlSerializer = xmlSerializer;
|
||||
_userManager = userManager;
|
||||
_dtoService = dtoService;
|
||||
_sessionMananger = sessionMananger;
|
||||
_config = config;
|
||||
_networkManager = networkManager;
|
||||
}
|
||||
|
||||
public object Get(GetPublicUsers request)
|
||||
{
|
||||
var authInfo = AuthorizationContext.GetAuthorizationInfo(Request);
|
||||
var isDashboard = string.Equals(authInfo.Client, "Dashboard", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if ((Request.IsLocal && isDashboard) ||
|
||||
!_config.Configuration.IsStartupWizardCompleted)
|
||||
{
|
||||
return Get(new GetUsers
|
||||
{
|
||||
IsDisabled = false
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Uncomment this once all clients can handle an empty user list.
|
||||
return Get(new GetUsers
|
||||
{
|
||||
IsHidden = false,
|
||||
IsDisabled = false
|
||||
});
|
||||
|
||||
//// TODO: Add or is authenticated
|
||||
//if (Request.IsLocal || IsInLocalNetwork(Request.RemoteIp))
|
||||
//{
|
||||
// return Get(new GetUsers
|
||||
// {
|
||||
// IsHidden = false,
|
||||
// IsDisabled = false
|
||||
// });
|
||||
//}
|
||||
|
||||
//// Return empty when external
|
||||
//return ToOptimizedResult(new List<UserDto>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -217,7 +245,7 @@ namespace MediaBrowser.Api
|
||||
|
||||
var result = users
|
||||
.OrderBy(u => u.Name)
|
||||
.Select(_dtoService.GetUserDto)
|
||||
.Select(i => _userManager.GetUserDto(i, Request.RemoteIp))
|
||||
.ToList();
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
@ -237,7 +265,7 @@ namespace MediaBrowser.Api
|
||||
throw new ResourceNotFoundException("User not found");
|
||||
}
|
||||
|
||||
var result = _dtoService.GetUserDto(user);
|
||||
var result = _userManager.GetUserDto(user, Request.RemoteIp);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
}
|
||||
@ -247,6 +275,13 @@ namespace MediaBrowser.Api
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public void Delete(DeleteUser request)
|
||||
{
|
||||
var task = DeleteAsync(request);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(DeleteUser request)
|
||||
{
|
||||
var user = _userManager.GetUserById(request.Id);
|
||||
|
||||
@ -255,9 +290,8 @@ namespace MediaBrowser.Api
|
||||
throw new ResourceNotFoundException("User not found");
|
||||
}
|
||||
|
||||
var task = _userManager.DeleteUser(user);
|
||||
|
||||
Task.WaitAll(task);
|
||||
await _sessionMananger.RevokeUserTokens(user.Id.ToString("N")).ConfigureAwait(false);
|
||||
await _userManager.DeleteUser(user).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -265,28 +299,6 @@ namespace MediaBrowser.Api
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public object Post(AuthenticateUser request)
|
||||
{
|
||||
// No response needed. Will throw an exception on failure.
|
||||
var result = AuthenticateUser(request).Result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public object Post(AuthenticateUserByName request)
|
||||
{
|
||||
var user = _userManager.Users.FirstOrDefault(i => string.Equals(request.Username, i.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentException(string.Format("User {0} not found.", request.Username));
|
||||
}
|
||||
|
||||
var result = AuthenticateUser(new AuthenticateUser { Id = user.Id, Password = request.Password }).Result;
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
private async Task<AuthenticationResult> AuthenticateUser(AuthenticateUser request)
|
||||
{
|
||||
var user = _userManager.GetUserById(request.Id);
|
||||
|
||||
@ -295,38 +307,47 @@ namespace MediaBrowser.Api
|
||||
throw new ResourceNotFoundException("User not found");
|
||||
}
|
||||
|
||||
var auth = AuthorizationRequestFilterAttribute.GetAuthorization(Request);
|
||||
|
||||
// Login in the old way if the header is missing
|
||||
if (string.IsNullOrEmpty(auth.Client) ||
|
||||
string.IsNullOrEmpty(auth.Device) ||
|
||||
string.IsNullOrEmpty(auth.DeviceId) ||
|
||||
string.IsNullOrEmpty(auth.Version))
|
||||
return Post(new AuthenticateUserByName
|
||||
{
|
||||
var success = await _userManager.AuthenticateUser(user, request.Password).ConfigureAwait(false);
|
||||
Username = user.Name,
|
||||
Password = request.Password
|
||||
});
|
||||
}
|
||||
|
||||
if (!success)
|
||||
{
|
||||
// Unauthorized
|
||||
throw new UnauthorizedAccessException("Invalid user or password entered.");
|
||||
}
|
||||
public async Task<object> Post(AuthenticateUserByName request)
|
||||
{
|
||||
var auth = AuthorizationContext.GetAuthorizationInfo(Request);
|
||||
|
||||
return new AuthenticationResult
|
||||
{
|
||||
User = _dtoService.GetUserDto(user)
|
||||
};
|
||||
if (string.IsNullOrWhiteSpace(auth.Client))
|
||||
{
|
||||
auth.Client = "Unknown app";
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(auth.Device))
|
||||
{
|
||||
auth.Device = "Unknown device";
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(auth.Version))
|
||||
{
|
||||
auth.Version = "Unknown version";
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(auth.DeviceId))
|
||||
{
|
||||
auth.DeviceId = "Unknown device id";
|
||||
}
|
||||
|
||||
var session = await _sessionMananger.AuthenticateNewSession(user, request.Password, auth.Client, auth.Version,
|
||||
auth.DeviceId, auth.Device, Request.RemoteIp).ConfigureAwait(false);
|
||||
|
||||
var result = new AuthenticationResult
|
||||
var result = await _sessionMananger.AuthenticateNewSession(new AuthenticationRequest
|
||||
{
|
||||
User = _dtoService.GetUserDto(user),
|
||||
SessionInfo = _sessionMananger.GetSessionInfoDto(session)
|
||||
};
|
||||
App = auth.Client,
|
||||
AppVersion = auth.Version,
|
||||
DeviceId = auth.DeviceId,
|
||||
DeviceName = auth.Device,
|
||||
Password = request.Password,
|
||||
RemoteEndPoint = Request.RemoteIp,
|
||||
Username = request.Username
|
||||
|
||||
return result;
|
||||
}, Request.IsLocal).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -334,6 +355,12 @@ namespace MediaBrowser.Api
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public void Post(UpdateUserPassword request)
|
||||
{
|
||||
var task = PostAsync(request);
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
public async Task PostAsync(UpdateUserPassword request)
|
||||
{
|
||||
var user = _userManager.GetUserById(request.Id);
|
||||
|
||||
@ -344,22 +371,18 @@ namespace MediaBrowser.Api
|
||||
|
||||
if (request.ResetPassword)
|
||||
{
|
||||
var task = _userManager.ResetPassword(user);
|
||||
|
||||
Task.WaitAll(task);
|
||||
await _userManager.ResetPassword(user).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var success = _userManager.AuthenticateUser(user, request.CurrentPassword).Result;
|
||||
var success = await _userManager.AuthenticateUser(user.Name, request.CurrentPassword, Request.RemoteIp).ConfigureAwait(false);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
throw new UnauthorizedAccessException("Invalid user or password entered.");
|
||||
throw new ArgumentException("Invalid user or password entered.");
|
||||
}
|
||||
|
||||
var task = _userManager.ChangePassword(user, request.NewPassword);
|
||||
|
||||
Task.WaitAll(task);
|
||||
await _userManager.ChangePassword(user, request.NewPassword).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -368,6 +391,13 @@ namespace MediaBrowser.Api
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public void Post(UpdateUser request)
|
||||
{
|
||||
var task = PostAsync(request);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
public async Task PostAsync(UpdateUser request)
|
||||
{
|
||||
// We need to parse this manually because we told service stack not to with IRequiresRequestStream
|
||||
// https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs
|
||||
@ -400,11 +430,15 @@ namespace MediaBrowser.Api
|
||||
{
|
||||
throw new ArgumentException("There must be at least one enabled user in the system.");
|
||||
}
|
||||
|
||||
await _sessionMananger.RevokeUserTokens(user.Id.ToString("N")).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var task = user.Name.Equals(dtoUser.Name, StringComparison.Ordinal) ? _userManager.UpdateUser(user) : _userManager.RenameUser(user, dtoUser.Name);
|
||||
var task = user.Name.Equals(dtoUser.Name, StringComparison.Ordinal) ?
|
||||
_userManager.UpdateUser(user) :
|
||||
_userManager.RenameUser(user, dtoUser.Name);
|
||||
|
||||
Task.WaitAll(task);
|
||||
await task.ConfigureAwait(false);
|
||||
|
||||
user.UpdateConfiguration(dtoUser.Configuration);
|
||||
}
|
||||
@ -422,7 +456,7 @@ namespace MediaBrowser.Api
|
||||
|
||||
newUser.UpdateConfiguration(dtoUser.Configuration);
|
||||
|
||||
var result = _dtoService.GetUserDto(newUser);
|
||||
var result = _userManager.GetUserDto(newUser, Request.RemoteIp);
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using ServiceStack;
|
||||
@ -41,6 +42,7 @@ namespace MediaBrowser.Api
|
||||
public string Ids { get; set; }
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class VideosService : BaseApiService
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
@ -97,12 +99,12 @@ namespace MediaBrowser.Api
|
||||
|
||||
public void Delete(DeleteAlternateSources request)
|
||||
{
|
||||
var task = RemoveAlternateVersions(request);
|
||||
var task = DeleteAsync(request);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
private async Task RemoveAlternateVersions(DeleteAlternateSources request)
|
||||
public async Task DeleteAsync(DeleteAlternateSources request)
|
||||
{
|
||||
var video = (Video)_libraryManager.GetItemById(request.Id);
|
||||
|
||||
@ -119,12 +121,12 @@ namespace MediaBrowser.Api
|
||||
|
||||
public void Post(MergeVersions request)
|
||||
{
|
||||
var task = MergeVersions(request);
|
||||
var task = PostAsync(request);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
private async Task MergeVersions(MergeVersions request)
|
||||
public async Task PostAsync(MergeVersions request)
|
||||
{
|
||||
var items = request.Ids.Split(',')
|
||||
.Select(i => new Guid(i))
|
||||
@ -170,12 +172,12 @@ namespace MediaBrowser.Api
|
||||
return 0;
|
||||
})
|
||||
.ThenByDescending(i =>
|
||||
{
|
||||
var stream = i.GetDefaultVideoStream();
|
||||
{
|
||||
var stream = i.GetDefaultVideoStream();
|
||||
|
||||
return stream == null || stream.Width == null ? 0 : stream.Width.Value;
|
||||
return stream == null || stream.Width == null ? 0 : stream.Width.Value;
|
||||
|
||||
}).First();
|
||||
}).First();
|
||||
}
|
||||
|
||||
foreach (var item in videos.Where(i => i.Id != primaryVersion.Id))
|
||||
|
@ -1,149 +0,0 @@
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Api.WebSocket
|
||||
{
|
||||
/// <summary>
|
||||
/// Class ScheduledTasksWebSocketListener
|
||||
/// </summary>
|
||||
public class LogFileWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<string>, LogFileWebSocketState>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
protected override string Name
|
||||
{
|
||||
get { return "LogFile"; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The _kernel
|
||||
/// </summary>
|
||||
private readonly ILogManager _logManager;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LogFileWebSocketListener" /> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="logManager">The log manager.</param>
|
||||
public LogFileWebSocketListener(ILogger logger, ILogManager logManager, IFileSystem fileSystem)
|
||||
: base(logger)
|
||||
{
|
||||
_logManager = logManager;
|
||||
_fileSystem = fileSystem;
|
||||
_logManager.LoggerLoaded += kernel_LoggerLoaded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data to send.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <returns>IEnumerable{System.String}.</returns>
|
||||
protected override async Task<IEnumerable<string>> GetDataToSend(LogFileWebSocketState state)
|
||||
{
|
||||
if (!string.Equals(_logManager.LogFilePath, state.LastLogFilePath))
|
||||
{
|
||||
state.LastLogFilePath = _logManager.LogFilePath;
|
||||
state.StartLine = 0;
|
||||
}
|
||||
|
||||
var lines = await GetLogLines(state.LastLogFilePath, state.StartLine, _fileSystem).ConfigureAwait(false);
|
||||
|
||||
state.StartLine += lines.Count;
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/// <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 override void Dispose(bool dispose)
|
||||
{
|
||||
if (dispose)
|
||||
{
|
||||
_logManager.LoggerLoaded -= kernel_LoggerLoaded;
|
||||
}
|
||||
base.Dispose(dispose);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the LoggerLoaded event of the kernel control.
|
||||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
|
||||
void kernel_LoggerLoaded(object sender, EventArgs e)
|
||||
{
|
||||
// Reset the startline for each connection whenever the logger reloads
|
||||
lock (ActiveConnections)
|
||||
{
|
||||
foreach (var connection in ActiveConnections)
|
||||
{
|
||||
connection.Item4.StartLine = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the log lines.
|
||||
/// </summary>
|
||||
/// <param name="logFilePath">The log file path.</param>
|
||||
/// <param name="startLine">The start line.</param>
|
||||
/// <returns>Task{IEnumerable{System.String}}.</returns>
|
||||
internal static async Task<List<string>> GetLogLines(string logFilePath, int startLine, IFileSystem fileSystem)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
|
||||
using (var fs = fileSystem.GetFileStream(logFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
|
||||
{
|
||||
using (var reader = new StreamReader(fs))
|
||||
{
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
var line = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||
|
||||
if (line.IndexOf("Info -", StringComparison.OrdinalIgnoreCase) != -1 ||
|
||||
line.IndexOf("Warn -", StringComparison.OrdinalIgnoreCase) != -1 ||
|
||||
line.IndexOf("Error -", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
lines.Add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startLine > 0)
|
||||
{
|
||||
lines = lines.Skip(startLine).ToList();
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class LogFileWebSocketState
|
||||
/// </summary>
|
||||
public class LogFileWebSocketState : WebSocketListenerState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the last log file path.
|
||||
/// </summary>
|
||||
/// <value>The last log file path.</value>
|
||||
public string LastLogFilePath { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the start line.
|
||||
/// </summary>
|
||||
/// <value>The start line.</value>
|
||||
public int StartLine { get; set; }
|
||||
}
|
||||
}
|
@ -211,6 +211,8 @@ namespace MediaBrowser.Common.Implementations
|
||||
JsonSerializer = CreateJsonSerializer();
|
||||
|
||||
Logger = LogManager.GetLogger("App");
|
||||
OnLoggerLoaded(true);
|
||||
LogManager.LoggerLoaded += (s, e) => OnLoggerLoaded(false);
|
||||
|
||||
IsFirstRun = !ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted;
|
||||
progress.Report(2);
|
||||
@ -219,16 +221,11 @@ namespace MediaBrowser.Common.Implementations
|
||||
? LogSeverity.Debug
|
||||
: LogSeverity.Info;
|
||||
|
||||
// Put the app config in the log for troubleshooting purposes
|
||||
Logger.LogMultiline("Application Configuration:", LogSeverity.Info, new StringBuilder(JsonSerializer.SerializeToString(ConfigurationManager.CommonConfiguration)));
|
||||
|
||||
progress.Report(3);
|
||||
|
||||
DiscoverTypes();
|
||||
progress.Report(14);
|
||||
|
||||
Logger.Info("Version {0} initializing", ApplicationVersion);
|
||||
|
||||
SetHttpLimit();
|
||||
progress.Report(15);
|
||||
|
||||
@ -245,6 +242,47 @@ namespace MediaBrowser.Common.Implementations
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
protected virtual void OnLoggerLoaded(bool isFirstLoad)
|
||||
{
|
||||
Logger.Info("Application version: {0}", ApplicationVersion);
|
||||
|
||||
if (!isFirstLoad)
|
||||
{
|
||||
LogEnvironmentInfo(Logger, ApplicationPaths);
|
||||
}
|
||||
|
||||
// Put the app config in the log for troubleshooting purposes
|
||||
Logger.LogMultiline("Application configuration:", LogSeverity.Info, new StringBuilder(JsonSerializer.SerializeToString(ConfigurationManager.CommonConfiguration)));
|
||||
|
||||
if (Plugins != null)
|
||||
{
|
||||
var pluginBuilder = new StringBuilder();
|
||||
|
||||
foreach (var plugin in Plugins)
|
||||
{
|
||||
pluginBuilder.AppendLine(string.Format("{0} {1}", plugin.Name, plugin.Version));
|
||||
}
|
||||
|
||||
Logger.LogMultiline("Plugins:", LogSeverity.Info, pluginBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
|
||||
{
|
||||
logger.Info("Command line: {0}", string.Join(" ", Environment.GetCommandLineArgs()));
|
||||
|
||||
logger.Info("Server: {0}", Environment.MachineName);
|
||||
logger.Info("Operating system: {0}", Environment.OSVersion.ToString());
|
||||
logger.Info("Processor count: {0}", Environment.ProcessorCount);
|
||||
logger.Info("64-Bit OS: {0}", Environment.Is64BitOperatingSystem);
|
||||
logger.Info("64-Bit Process: {0}", Environment.Is64BitProcess);
|
||||
logger.Info("Program data path: {0}", appPaths.ProgramDataPath);
|
||||
|
||||
logger.Info("Application Path: {0}", appPaths.ApplicationPath);
|
||||
|
||||
logger.Info("*** When reporting issues please include the entire log file. ***".ToUpper());
|
||||
}
|
||||
|
||||
protected virtual IJsonSerializer CreateJsonSerializer()
|
||||
{
|
||||
return new JsonSerializer();
|
||||
@ -342,6 +380,7 @@ namespace MediaBrowser.Common.Implementations
|
||||
/// </summary>
|
||||
protected virtual void FindParts()
|
||||
{
|
||||
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
||||
Plugins = GetExports<IPlugin>();
|
||||
}
|
||||
|
||||
@ -393,7 +432,7 @@ namespace MediaBrowser.Common.Implementations
|
||||
HttpClient = new HttpClientManager.HttpClientManager(ApplicationPaths, Logger, FileSystemManager, ConfigurationManager);
|
||||
RegisterSingleInstance(HttpClient);
|
||||
|
||||
NetworkManager = CreateNetworkManager();
|
||||
NetworkManager = CreateNetworkManager(LogManager.GetLogger("NetworkManager"));
|
||||
RegisterSingleInstance(NetworkManager);
|
||||
|
||||
SecurityManager = new PluginSecurityManager(this, HttpClient, JsonSerializer, ApplicationPaths, NetworkManager, LogManager);
|
||||
@ -461,7 +500,7 @@ namespace MediaBrowser.Common.Implementations
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract INetworkManager CreateNetworkManager();
|
||||
protected abstract INetworkManager CreateNetworkManager(ILogger logger);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of type and resolves all constructor dependancies
|
||||
@ -631,6 +670,7 @@ namespace MediaBrowser.Common.Implementations
|
||||
return parts;
|
||||
}
|
||||
|
||||
private Version _version;
|
||||
/// <summary>
|
||||
/// Gets the current application version
|
||||
/// </summary>
|
||||
@ -639,7 +679,7 @@ namespace MediaBrowser.Common.Implementations
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetType().Assembly.GetName().Version;
|
||||
return _version ?? (_version = GetType().Assembly.GetName().Version);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,13 @@
|
||||
using System.IO;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Events;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace MediaBrowser.Common.Implementations.Configuration
|
||||
@ -25,6 +28,16 @@ namespace MediaBrowser.Common.Implementations.Configuration
|
||||
/// </summary>
|
||||
public event EventHandler<EventArgs> ConfigurationUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [configuration updating].
|
||||
/// </summary>
|
||||
public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [named configuration updated].
|
||||
/// </summary>
|
||||
public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the logger.
|
||||
/// </summary>
|
||||
@ -74,6 +87,9 @@ namespace MediaBrowser.Common.Implementations.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
private ConfigurationStore[] _configurationStores = {};
|
||||
private IConfigurationFactory[] _configurationFactories;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class.
|
||||
/// </summary>
|
||||
@ -89,10 +105,14 @@ namespace MediaBrowser.Common.Implementations.Configuration
|
||||
UpdateCachePath();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The _save lock
|
||||
/// </summary>
|
||||
private readonly object _configurationSaveLock = new object();
|
||||
public void AddParts(IEnumerable<IConfigurationFactory> factories)
|
||||
{
|
||||
_configurationFactories = factories.ToArray();
|
||||
|
||||
_configurationStores = _configurationFactories
|
||||
.SelectMany(i => i.GetConfigurations())
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the configuration.
|
||||
@ -103,7 +123,7 @@ namespace MediaBrowser.Common.Implementations.Configuration
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
|
||||
lock (_configurationSaveLock)
|
||||
lock (_configurationSyncLock)
|
||||
{
|
||||
XmlSerializer.SerializeToFile(CommonConfiguration, path);
|
||||
}
|
||||
@ -168,5 +188,70 @@ namespace MediaBrowser.Common.Implementations.Configuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
|
||||
|
||||
private string GetConfigurationFile(string key)
|
||||
{
|
||||
return Path.Combine(CommonApplicationPaths.ConfigurationDirectoryPath, key.ToLower() + ".xml");
|
||||
}
|
||||
|
||||
public object GetConfiguration(string key)
|
||||
{
|
||||
return _configurations.GetOrAdd(key, k =>
|
||||
{
|
||||
var file = GetConfigurationFile(key);
|
||||
|
||||
var configurationType = _configurationStores
|
||||
.First(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
.ConfigurationType;
|
||||
|
||||
lock (_configurationSyncLock)
|
||||
{
|
||||
return ConfigurationHelper.GetXmlConfiguration(configurationType, file, XmlSerializer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void SaveConfiguration(string key, object configuration)
|
||||
{
|
||||
var configurationType = GetConfigurationType(key);
|
||||
|
||||
if (configuration.GetType() != configurationType)
|
||||
{
|
||||
throw new ArgumentException("Expected configuration type is " + configurationType.Name);
|
||||
}
|
||||
|
||||
EventHelper.FireEventIfNotNull(NamedConfigurationUpdating, this, new ConfigurationUpdateEventArgs
|
||||
{
|
||||
Key = key,
|
||||
NewConfiguration = configuration
|
||||
|
||||
}, Logger);
|
||||
|
||||
_configurations.AddOrUpdate(key, configuration, (k, v) => configuration);
|
||||
|
||||
var path = GetConfigurationFile(key);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
|
||||
lock (_configurationSyncLock)
|
||||
{
|
||||
XmlSerializer.SerializeToFile(configuration, path);
|
||||
}
|
||||
|
||||
EventHelper.FireEventIfNotNull(NamedConfigurationUpdated, this, new ConfigurationUpdateEventArgs
|
||||
{
|
||||
Key = key,
|
||||
NewConfiguration = configuration
|
||||
|
||||
}, Logger);
|
||||
}
|
||||
|
||||
public Type GetConfigurationType(string key)
|
||||
{
|
||||
return _configurationStores
|
||||
.First(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
.ConfigurationType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +107,6 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
|
||||
return client;
|
||||
}
|
||||
|
||||
private PropertyInfo _httpBehaviorPropertyInfo;
|
||||
private WebRequest GetRequest(HttpRequestOptions options, string method, bool enableHttpCompression)
|
||||
{
|
||||
var request = (HttpWebRequest)WebRequest.Create(options.Url);
|
||||
@ -118,7 +117,11 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
|
||||
|
||||
request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
|
||||
|
||||
request.KeepAlive = options.EnableKeepAlive;
|
||||
if (options.EnableKeepAlive)
|
||||
{
|
||||
request.KeepAlive = true;
|
||||
}
|
||||
|
||||
request.Method = method;
|
||||
request.Pipelined = true;
|
||||
request.Timeout = 20000;
|
||||
@ -133,21 +136,6 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
|
||||
request.Referer = options.Referer;
|
||||
}
|
||||
|
||||
#if !__MonoCS__
|
||||
if (options.EnableKeepAlive)
|
||||
{
|
||||
// This is a hack to prevent KeepAlive from getting disabled internally by the HttpWebRequest
|
||||
// May need to remove this for mono
|
||||
var sp = request.ServicePoint;
|
||||
if (_httpBehaviorPropertyInfo == null)
|
||||
{
|
||||
_httpBehaviorPropertyInfo = sp.GetType().GetProperty("HttpBehaviour", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
}
|
||||
|
||||
_httpBehaviorPropertyInfo.SetValue(sp, (byte)0, null);
|
||||
}
|
||||
#endif
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
|
@ -367,5 +367,20 @@ namespace MediaBrowser.Common.Implementations.IO
|
||||
|
||||
return newPath;
|
||||
}
|
||||
|
||||
public string GetFileNameWithoutExtension(FileSystemInfo info)
|
||||
{
|
||||
if (info is DirectoryInfo)
|
||||
{
|
||||
return info.Name;
|
||||
}
|
||||
|
||||
return Path.GetFileNameWithoutExtension(info.FullName);
|
||||
}
|
||||
|
||||
public string GetFileNameWithoutExtension(string path)
|
||||
{
|
||||
return Path.GetFileNameWithoutExtension(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -48,17 +48,17 @@
|
||||
<RunPostBuildEvent>Always</RunPostBuildEvent>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="NLog, Version=3.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
|
||||
<Reference Include="NLog, Version=3.1.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\NLog.3.0.0.0\lib\net45\NLog.dll</HintPath>
|
||||
<HintPath>..\packages\NLog.3.1.0.0\lib\net45\NLog.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SimpleInjector, Version=2.5.0.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\SimpleInjector.2.5.0\lib\net45\SimpleInjector.dll</HintPath>
|
||||
<HintPath>..\packages\SimpleInjector.2.5.2\lib\net45\SimpleInjector.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SimpleInjector.Diagnostics, Version=2.5.0.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
|
||||
<Reference Include="SimpleInjector.Diagnostics, Version=2.5.2.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\SimpleInjector.2.5.0\lib\net45\SimpleInjector.Diagnostics.dll</HintPath>
|
||||
<HintPath>..\packages\SimpleInjector.2.5.2\lib\net45\SimpleInjector.Diagnostics.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Configuration" />
|
||||
@ -122,7 +122,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="$(SolutionDir)\.nuget\nuget.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " />
|
||||
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" />
|
||||
<PropertyGroup>
|
||||
<PostBuildEvent Condition=" '$(ConfigurationName)' != 'Release Mono' ">if '$(ConfigurationName)' == 'Release' (
|
||||
xcopy "$(TargetPath)" "$(SolutionDir)\Nuget\dlls\" /y /d /r /i
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
@ -10,6 +11,13 @@ namespace MediaBrowser.Common.Implementations.Networking
|
||||
{
|
||||
public abstract class BaseNetworkManager
|
||||
{
|
||||
protected ILogger Logger { get; private set; }
|
||||
|
||||
protected BaseNetworkManager(ILogger logger)
|
||||
{
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the machine's local ip address
|
||||
/// </summary>
|
||||
@ -26,6 +34,81 @@ namespace MediaBrowser.Common.Implementations.Networking
|
||||
return GetLocalIpAddressesFallback();
|
||||
}
|
||||
|
||||
public bool IsInLocalNetwork(string endpoint)
|
||||
{
|
||||
return IsInLocalNetworkInternal(endpoint, true);
|
||||
}
|
||||
|
||||
public bool IsInLocalNetworkInternal(string endpoint, bool resolveHost)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(endpoint))
|
||||
{
|
||||
throw new ArgumentNullException("endpoint");
|
||||
}
|
||||
|
||||
const int lengthMatch = 4;
|
||||
|
||||
if (endpoint.Length >= lengthMatch)
|
||||
{
|
||||
var prefix = endpoint.Substring(0, lengthMatch);
|
||||
|
||||
if (GetLocalIpAddresses()
|
||||
.Any(i => i.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Private address space:
|
||||
// http://en.wikipedia.org/wiki/Private_network
|
||||
|
||||
var isPrivate =
|
||||
|
||||
// If url was requested with computer name, we may see this
|
||||
endpoint.IndexOf("::", StringComparison.OrdinalIgnoreCase) != -1 ||
|
||||
|
||||
endpoint.StartsWith("10.", StringComparison.OrdinalIgnoreCase) ||
|
||||
endpoint.StartsWith("192.", StringComparison.OrdinalIgnoreCase) ||
|
||||
endpoint.StartsWith("172.", StringComparison.OrdinalIgnoreCase) ||
|
||||
endpoint.StartsWith("169.", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (isPrivate)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
IPAddress address;
|
||||
if (resolveHost && !IPAddress.TryParse(endpoint, out address))
|
||||
{
|
||||
var host = new Uri(endpoint).DnsSafeHost;
|
||||
|
||||
Logger.Debug("Resolving host {0}", host);
|
||||
|
||||
try
|
||||
{
|
||||
address = GetIpAddresses(host).FirstOrDefault();
|
||||
|
||||
if (address != null)
|
||||
{
|
||||
Logger.Debug("{0} resolved to {1}", host, address);
|
||||
|
||||
return IsInLocalNetworkInternal(address.ToString(), false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.ErrorException("Error resovling hostname {0}", ex, host);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public IEnumerable<IPAddress> GetIpAddresses(string hostName)
|
||||
{
|
||||
return Dns.GetHostAddresses(hostName);
|
||||
}
|
||||
|
||||
private IEnumerable<IPAddress> GetIPsDefault()
|
||||
{
|
||||
foreach (var adapter in NetworkInterface.GetAllNetworkInterfaces())
|
||||
|
@ -547,6 +547,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
||||
if (ex != null)
|
||||
{
|
||||
result.ErrorMessage = ex.Message;
|
||||
result.LongErrorMessage = ex.StackTrace;
|
||||
}
|
||||
|
||||
var path = GetHistoryFilePath();
|
||||
|
@ -46,7 +46,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
|
||||
return new ITaskTrigger[] {
|
||||
|
||||
// At startup
|
||||
new StartupTrigger (),
|
||||
new StartupTrigger {DelayMs = 60000},
|
||||
|
||||
// Every so often
|
||||
new IntervalTrigger { Interval = TimeSpan.FromHours(24)}
|
||||
|
@ -43,7 +43,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
|
||||
return new ITaskTrigger[] {
|
||||
|
||||
// At startup
|
||||
new StartupTrigger (),
|
||||
new StartupTrigger {DelayMs = 30000},
|
||||
|
||||
// Every so often
|
||||
new IntervalTrigger { Interval = TimeSpan.FromHours(24)}
|
||||
|
@ -41,7 +41,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
|
||||
return new ITaskTrigger[] {
|
||||
|
||||
// At startup
|
||||
new StartupTrigger (),
|
||||
new StartupTrigger(),
|
||||
|
||||
// Every so often
|
||||
new IntervalTrigger { Interval = TimeSpan.FromHours(24)}
|
||||
|
@ -52,7 +52,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
|
||||
return new ITaskTrigger[] {
|
||||
|
||||
// At startup
|
||||
new StartupTrigger (),
|
||||
new StartupTrigger(),
|
||||
|
||||
// Every so often
|
||||
new IntervalTrigger { Interval = TimeSpan.FromHours(24)}
|
||||
|
@ -26,16 +26,13 @@ namespace MediaBrowser.Common.Implementations.Security
|
||||
|
||||
var mac = _networkManager.GetMacAddress();
|
||||
|
||||
var plugins = string.Join("|", _applicationHost.Plugins.Select(i => i.Name).ToArray());
|
||||
|
||||
var data = new Dictionary<string, string>
|
||||
{
|
||||
{ "feature", _applicationHost.Name },
|
||||
{ "mac", mac },
|
||||
{ "ver", _applicationHost.ApplicationVersion.ToString() },
|
||||
{ "platform", Environment.OSVersion.VersionString },
|
||||
{ "isservice", _applicationHost.IsRunningAsService.ToString().ToLower()},
|
||||
{ "plugins", plugins}
|
||||
{ "isservice", _applicationHost.IsRunningAsService.ToString().ToLower()}
|
||||
};
|
||||
|
||||
return _httpClient.Post(Constants.Constants.MbAdminUrl + "service/registration/ping", data, cancellationToken);
|
||||
|
@ -210,25 +210,5 @@ namespace MediaBrowser.Common.Implementations.Serialization
|
||||
|
||||
return ServiceStack.Text.JsonSerializer.SerializeToString(obj, obj.GetType());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes to bytes.
|
||||
/// </summary>
|
||||
/// <param name="obj">The obj.</param>
|
||||
/// <returns>System.Byte[][].</returns>
|
||||
/// <exception cref="System.ArgumentNullException">obj</exception>
|
||||
public byte[] SerializeToBytes(object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
throw new ArgumentNullException("obj");
|
||||
}
|
||||
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
SerializeToStream(obj, stream);
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,20 +91,5 @@ namespace MediaBrowser.Common.Implementations.Serialization
|
||||
return DeserializeFromStream(type, stream);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes to bytes.
|
||||
/// </summary>
|
||||
/// <param name="obj">The obj.</param>
|
||||
/// <returns>System.Byte[][].</returns>
|
||||
public byte[] SerializeToBytes(object obj)
|
||||
{
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
SerializeToStream(obj, stream);
|
||||
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ namespace MediaBrowser.Common.Implementations.Updates
|
||||
/// <param name="newVersion">The new version.</param>
|
||||
private void OnPluginUpdated(IPlugin plugin, PackageVersionInfo newVersion)
|
||||
{
|
||||
_logger.Info("Plugin updated: {0} {1} {2}", newVersion.name, newVersion.version, newVersion.classification);
|
||||
_logger.Info("Plugin updated: {0} {1} {2}", newVersion.name, newVersion.versionStr ?? string.Empty, newVersion.classification);
|
||||
|
||||
EventHelper.FireEventIfNotNull(PluginUpdated, this, new GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>> { Argument = new Tuple<IPlugin, PackageVersionInfo>(plugin, newVersion) }, _logger);
|
||||
|
||||
@ -87,7 +87,7 @@ namespace MediaBrowser.Common.Implementations.Updates
|
||||
/// <param name="package">The package.</param>
|
||||
private void OnPluginInstalled(PackageVersionInfo package)
|
||||
{
|
||||
_logger.Info("New plugin installed: {0} {1} {2}", package.name, package.version, package.classification);
|
||||
_logger.Info("New plugin installed: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification);
|
||||
|
||||
EventHelper.FireEventIfNotNull(PluginInstalled, this, new GenericEventArgs<PackageVersionInfo> { Argument = package }, _logger);
|
||||
|
||||
@ -133,6 +133,16 @@ namespace MediaBrowser.Common.Implementations.Updates
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private Version GetPackageVersion(PackageVersionInfo version)
|
||||
{
|
||||
return new Version(ValueOrDefault(version.versionStr, "0.0.0.1"));
|
||||
}
|
||||
|
||||
private static string ValueOrDefault(string str, string def)
|
||||
{
|
||||
return string.IsNullOrEmpty(str) ? def : str;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all available packages.
|
||||
/// </summary>
|
||||
@ -167,10 +177,20 @@ namespace MediaBrowser.Common.Implementations.Updates
|
||||
{
|
||||
if (_lastPackageListResult != null)
|
||||
{
|
||||
// Let dev users get results more often for testing purposes
|
||||
var cacheLength = _config.CommonConfiguration.SystemUpdateLevel == PackageVersionClass.Dev
|
||||
? TimeSpan.FromMinutes(3)
|
||||
: TimeSpan.FromHours(6);
|
||||
TimeSpan cacheLength;
|
||||
|
||||
switch (_config.CommonConfiguration.SystemUpdateLevel)
|
||||
{
|
||||
case PackageVersionClass.Beta:
|
||||
cacheLength = TimeSpan.FromMinutes(30);
|
||||
break;
|
||||
case PackageVersionClass.Dev:
|
||||
cacheLength = TimeSpan.FromMinutes(3);
|
||||
break;
|
||||
default:
|
||||
cacheLength = TimeSpan.FromHours(6);
|
||||
break;
|
||||
}
|
||||
|
||||
if ((DateTime.UtcNow - _lastPackageListResult.Item2) < cacheLength)
|
||||
{
|
||||
@ -197,7 +217,7 @@ namespace MediaBrowser.Common.Implementations.Updates
|
||||
foreach (var package in packages)
|
||||
{
|
||||
package.versions = package.versions.Where(v => !string.IsNullOrWhiteSpace(v.sourceUrl))
|
||||
.OrderByDescending(v => v.version).ToList();
|
||||
.OrderByDescending(GetPackageVersion).ToList();
|
||||
}
|
||||
|
||||
// Remove packages with no versions
|
||||
@ -211,7 +231,7 @@ namespace MediaBrowser.Common.Implementations.Updates
|
||||
foreach (var package in packages)
|
||||
{
|
||||
package.versions = package.versions.Where(v => !string.IsNullOrWhiteSpace(v.sourceUrl))
|
||||
.OrderByDescending(v => v.version).ToList();
|
||||
.OrderByDescending(GetPackageVersion).ToList();
|
||||
}
|
||||
|
||||
if (packageType.HasValue)
|
||||
@ -272,7 +292,7 @@ namespace MediaBrowser.Common.Implementations.Updates
|
||||
return null;
|
||||
}
|
||||
|
||||
return package.versions.FirstOrDefault(v => v.version.Equals(version) && v.classification == classification);
|
||||
return package.versions.FirstOrDefault(v => GetPackageVersion(v).Equals(version) && v.classification == classification);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -309,7 +329,7 @@ namespace MediaBrowser.Common.Implementations.Updates
|
||||
}
|
||||
|
||||
return package.versions
|
||||
.OrderByDescending(v => v.version)
|
||||
.OrderByDescending(GetPackageVersion)
|
||||
.FirstOrDefault(v => v.classification <= classification && IsPackageVersionUpToDate(v, currentServerVersion));
|
||||
}
|
||||
|
||||
@ -338,7 +358,7 @@ namespace MediaBrowser.Common.Implementations.Updates
|
||||
{
|
||||
var latestPluginInfo = GetLatestCompatibleVersion(catalog, p.Name, p.Id.ToString(), applicationVersion, _config.CommonConfiguration.SystemUpdateLevel);
|
||||
|
||||
return latestPluginInfo != null && latestPluginInfo.version != null && latestPluginInfo.version > p.Version ? latestPluginInfo : null;
|
||||
return latestPluginInfo != null && GetPackageVersion(latestPluginInfo) > p.Version ? latestPluginInfo : null;
|
||||
|
||||
}).Where(i => i != null).ToList();
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="NLog" version="3.0.0.0" targetFramework="net45" />
|
||||
<package id="NLog" version="3.1.0.0" targetFramework="net45" />
|
||||
<package id="sharpcompress" version="0.10.2" targetFramework="net45" />
|
||||
<package id="SimpleInjector" version="2.5.0" targetFramework="net45" />
|
||||
<package id="SimpleInjector" version="2.5.2" targetFramework="net45" />
|
||||
</packages>
|
@ -36,22 +36,26 @@ namespace MediaBrowser.Common.Configuration
|
||||
configuration = Activator.CreateInstance(type);
|
||||
}
|
||||
|
||||
// Take the object we just got and serialize it back to bytes
|
||||
var newBytes = xmlSerializer.SerializeToBytes(configuration);
|
||||
|
||||
// If the file didn't exist before, or if something has changed, re-save
|
||||
if (buffer == null || !buffer.SequenceEqual(newBytes))
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
xmlSerializer.SerializeToStream(configuration, stream);
|
||||
|
||||
// Save it after load in case we got new items
|
||||
File.WriteAllBytes(path, newBytes);
|
||||
// Take the object we just got and serialize it back to bytes
|
||||
var newBytes = stream.ToArray();
|
||||
|
||||
// If the file didn't exist before, or if something has changed, re-save
|
||||
if (buffer == null || !buffer.SequenceEqual(newBytes))
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
|
||||
// Save it after load in case we got new items
|
||||
File.WriteAllBytes(path, newBytes);
|
||||
}
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Reads an xml configuration file from the file system
|
||||
/// It will immediately save the configuration after loading it, just
|
||||
|
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Common.Configuration
|
||||
{
|
||||
public class ConfigurationUpdateEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the key.
|
||||
/// </summary>
|
||||
/// <value>The key.</value>
|
||||
public string Key { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the new configuration.
|
||||
/// </summary>
|
||||
/// <value>The new configuration.</value>
|
||||
public object NewConfiguration { get; set; }
|
||||
}
|
||||
}
|
17
MediaBrowser.Common/Configuration/IConfigurationFactory.cs
Normal file
17
MediaBrowser.Common/Configuration/IConfigurationFactory.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Common.Configuration
|
||||
{
|
||||
public interface IConfigurationFactory
|
||||
{
|
||||
IEnumerable<ConfigurationStore> GetConfigurations();
|
||||
}
|
||||
|
||||
public class ConfigurationStore
|
||||
{
|
||||
public string Key { get; set; }
|
||||
|
||||
public Type ConfigurationType { get; set; }
|
||||
}
|
||||
}
|
@ -1,15 +1,26 @@
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Common.Configuration
|
||||
{
|
||||
public interface IConfigurationManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when [configuration updating].
|
||||
/// </summary>
|
||||
event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [configuration updated].
|
||||
/// </summary>
|
||||
event EventHandler<EventArgs> ConfigurationUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [named configuration updated].
|
||||
/// </summary>
|
||||
event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the application paths.
|
||||
/// </summary>
|
||||
@ -32,5 +43,40 @@ namespace MediaBrowser.Common.Configuration
|
||||
/// </summary>
|
||||
/// <param name="newConfiguration">The new configuration.</param>
|
||||
void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration.
|
||||
/// </summary>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
object GetConfiguration(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the configuration.
|
||||
/// </summary>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>Type.</returns>
|
||||
Type GetConfigurationType(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the configuration.
|
||||
/// </summary>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="configuration">The configuration.</param>
|
||||
void SaveConfiguration(string key, object configuration);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the parts.
|
||||
/// </summary>
|
||||
/// <param name="factories">The factories.</param>
|
||||
void AddParts(IEnumerable<IConfigurationFactory> factories);
|
||||
}
|
||||
|
||||
public static class ConfigurationManagerExtensions
|
||||
{
|
||||
public static T GetConfiguration<T>(this IConfigurationManager manager, string key)
|
||||
{
|
||||
return (T)manager.GetConfiguration(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,5 +112,19 @@ namespace MediaBrowser.Common.IO
|
||||
/// <param name="to">To.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
string SubstitutePath(string path, string from, string to);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file name without extension.
|
||||
/// </summary>
|
||||
/// <param name="info">The information.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
string GetFileNameWithoutExtension(FileSystemInfo info);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file name without extension.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
string GetFileNameWithoutExtension(string path);
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,9 @@
|
||||
<Link>Properties\SharedVersion.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="Configuration\ConfigurationHelper.cs" />
|
||||
<Compile Include="Configuration\ConfigurationUpdateEventArgs.cs" />
|
||||
<Compile Include="Configuration\IConfigurationManager.cs" />
|
||||
<Compile Include="Configuration\IConfigurationFactory.cs" />
|
||||
<Compile Include="Constants\Constants.cs" />
|
||||
<Compile Include="Events\EventHelper.cs" />
|
||||
<Compile Include="Extensions\BaseExtensions.cs" />
|
||||
@ -76,7 +78,6 @@
|
||||
<Compile Include="Net\INetworkManager.cs" />
|
||||
<Compile Include="Net\IWebSocket.cs" />
|
||||
<Compile Include="Net\IWebSocketConnection.cs" />
|
||||
<Compile Include="Net\IWebSocketServer.cs" />
|
||||
<Compile Include="Net\MimeTypes.cs" />
|
||||
<Compile Include="Net\WebSocketConnectEventArgs.cs" />
|
||||
<Compile Include="Net\WebSocketMessageInfo.cs" />
|
||||
@ -115,7 +116,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="$(SolutionDir)\.nuget\nuget.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " />
|
||||
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" />
|
||||
<PropertyGroup>
|
||||
<PostBuildEvent Condition=" '$(ConfigurationName)' != 'Release Mono' ">if '$(ConfigurationName)' == 'Release' (
|
||||
xcopy "$(TargetPath)" "$(SolutionDir)\Nuget\dlls\" /y /d /r /i
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user