Merge pull request #1 from MediaBrowser/master

Update to latest
pull/702/head
7illusions 11 years ago
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));
}
if (jobs.Count == 0)
{
return;
}
if (acquireLock)
{
await TranscodingStartLock.WaitAsync(CancellationToken.None).ConfigureAwait(false);
}
foreach (var job in jobs)
try
{
foreach (var job in jobs)
{
KillTranscodingJob(job, deleteFiles);
}
}
finally
{
KillTranscodingJob(job, deleteFiles);
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.
/// &lt;0 Executed before global request filters
/// &gt;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 =>

@ -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
{
@ -172,7 +174,8 @@ namespace MediaBrowser.Api
[ApiMember(Name = "UserId", Description = "Optional attach user data.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
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,95 +115,93 @@ namespace MediaBrowser.Api
return ToOptimizedResultUsingCache(cacheKey, dateModified, null, () => _configurationManager.Configuration);
}
/// <summary>
/// Posts the specified configuraiton.
/// </summary>
/// <param name="request">The request.</param>
public void Post(UpdateConfiguration request)
public object Get(GetNamedConfiguration request)
{
// Silly, but we need to serialize and deserialize or the XmlSerializer will write the xml with an element name of UpdateConfiguration
var result = _configurationManager.GetConfiguration(request.Key);
var json = _jsonSerializer.SerializeToString(request);
var config = _jsonSerializer.DeserializeFromString<ServerConfiguration>(json);
_configurationManager.ReplaceConfiguration(config);
return ToOptimizedResult(result);
}
public object Get(GetDefaultMetadataOptions request)
{
return ToOptimizedSerializedResultUsingCache(new MetadataOptions());
}
const string XbmcMetadata = "Xbmc Nfo";
const string MediaBrowserMetadata = "Media Browser Xml";
public object Get(GetMetadataPlugins request)
public void Post(AutoSetMetadataOptions request)
{
return ToOptimizedSerializedResultUsingCache(_providerManager.GetAllMetadataPlugins().ToList());
}
var service = AutoDetectMetadataService();
/// <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;
Logger.Info("Setting preferred metadata format to " + service);
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);
var serviceToDisable = string.Equals(service, XbmcMetadata) ?
MediaBrowserMetadata :
XbmcMetadata;
_configurationManager.DisableMetadataService(serviceToDisable);
_configurationManager.SaveConfiguration();
}
private void EnableImageExtractionForType(Type type, ServerConfiguration config, bool enabled)
private string AutoDetectMetadataService()
{
var options = GetMetadataOptions(type, config);
const string imageProviderName = "Screen Grabber";
var contains = options.DisabledImageFetchers.Contains(imageProviderName, StringComparer.OrdinalIgnoreCase);
if (!enabled && !contains)
try
{
var list = options.DisabledImageFetchers.ToList();
list.Add(imageProviderName);
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;
}
options.DisabledImageFetchers = list.ToArray();
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;
}
}
else if (enabled && contains)
catch (Exception)
{
var list = options.DisabledImageFetchers.ToList();
list.Remove(imageProviderName);
options.DisabledImageFetchers = list.ToArray();
}
return XbmcMetadata;
}
private MetadataOptions GetMetadataOptions(Type type, ServerConfiguration config)
/// <summary>
/// Posts the specified configuraiton.
/// </summary>
/// <param name="request">The request.</param>
public void Post(UpdateConfiguration request)
{
var options = config.MetadataOptions
.FirstOrDefault(i => string.Equals(i.ItemType, type.Name, StringComparison.OrdinalIgnoreCase));
// 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);
if (options == null)
{
var list = config.MetadataOptions.ToList();
var config = _jsonSerializer.DeserializeFromString<ServerConfiguration>(json);
options = new MetadataOptions
{
ItemType = type.Name
};
_configurationManager.ReplaceConfiguration(config);
}
list.Add(options);
public void Post(UpdateNamedConfiguration request)
{
var pathInfo = PathInfo.Parse(Request.PathInfo);
var key = pathInfo.GetArgumentValue<string>(2);
config.MetadataOptions = list.ToArray();
}
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());
}
return options;
public object Get(GetMetadataPlugins request)
{
return ToOptimizedSerializedResultUsingCache(_providerManager.GetAllMetadataPlugins().ToList());
}
}
}

@ -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>
@ -355,49 +361,25 @@ namespace MediaBrowser.Api.Images
/// <returns>System.Object.</returns>
public object Get(GetItemImage request)
{
var item = string.IsNullOrEmpty(request.Id) ?
var item = string.IsNullOrEmpty(request.Id) ?
_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(';');
for (var i = 0; i < vals.Length; i++)
{
var val = vals[i];
if (string.IsNullOrWhiteSpace(val))
{
continue;
}
var item = string.IsNullOrEmpty(request.Id) ?
_libraryManager.RootFolder :
_libraryManager.GetItemById(request.Id);
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
return GetImageResult(item,
request,
imageInfo,
supportedImageEnhancers,
contentType,
cacheDuration,
responseHeaders,
isHeadRequest)
.Result;
}
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)
{
Item = currentItem,
Request = currentRequest,
Enhancers = supportedImageEnhancers,
Image = imageInfo,
ImageProcessor = _imageProcessor
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
};
}, contentType, responseHeaders);
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());
@ -356,7 +347,7 @@ namespace MediaBrowser.Api.Images
return ToStaticFileResult(contentPath);
}
/// <summary>
/// Downloads the image.
/// </summary>

@ -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;
@ -91,7 +99,7 @@ namespace MediaBrowser.Api
private async Task RefreshItem(RefreshItem request, BaseItem item)
{
var options = GetRefreshOptions(request);
try
{
await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
@ -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;
@ -130,36 +131,11 @@ namespace MediaBrowser.Api.Library
/// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
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))

@ -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();
}
}
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);
}
}
int audioBitrate;
int videoBitrate;
GetPlaylistBitrates(state, out audioBitrate, out videoBitrate);
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;
if (CountStringOccurrences(fileText, "#EXTINF:") >= segmentCount)
{
break;
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);
}
}
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>
@ -276,10 +251,10 @@ namespace MediaBrowser.Api.Playback.Hls
protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
{
var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream;
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 Get(GetDynamicHlsVideoSegment request)
public object Head(GetMasterHlsVideoStream request)
{
if (string.Equals("baseline", request.PlaylistId, StringComparison.OrdinalIgnoreCase))
{
return GetDynamicSegment(request, false).Result;
}
var result = GetAsync(request, "HEAD").Result;
return GetDynamicSegment(request, true).Result;
return result;
}
private static readonly SemaphoreSlim FfmpegStartLock = new SemaphoreSlim(1, 1);
private async Task<object> GetDynamicSegment(GetDynamicHlsVideoSegment request, bool isMain)
public object Get(GetMainHlsVideoStream request)
{
var result = GetPlaylistAsync(request, "main").Result;
return result;
}
public object Get(GetDynamicHlsVideoSegment request)
{
return GetDynamicSegment(request, request.SegmentId).Result;
}
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);
}
private int GetStartNumber(VideoStreamRequest request)
{
var segmentId = "0";
var segmentRequest = request as GetDynamicHlsVideoSegment;
if (segmentRequest != null)
{
segmentId = segmentRequest.SegmentId;
}
return int.Parse(request.SegmentId, NumberStyles.Integer, UsCulture);
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)
{
// 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 bool IsTranscoding(string playlistPath)
{
// TODO: Handle if it's currently being written to
return ResultFactory.GetStaticFileResult(Request, path, FileShare.ReadWrite);
var job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType);
return job != null && !job.HasExited;
}
private async Task<object> GetAsync(GetMasterHlsVideoStream request)
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);
if (string.Equals(request.AudioCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException("Audio codec copy is not allowed here.");
}
var appendBaselineStream = false;
var baselineStreamBitrate = 64000;
if (string.Equals(request.VideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException("Video codec copy is not allowed here.");
}
var hlsVideoRequest = state.VideoRequest as GetMasterHlsVideoStream;
if (hlsVideoRequest != null)
if (string.IsNullOrEmpty(request.MediaSourceId))
{
appendBaselineStream = hlsVideoRequest.AppendBaselineStream;
baselineStreamBitrate = hlsVideoRequest.BaselineStreamAudioBitRate ?? baselineStreamBitrate;
throw new ArgumentException("MediaSourceId is required");
}
var playlistText = GetMasterPlaylistFileText(videoBitrate + audioBitrate, appendBaselineStream, baselineStreamBitrate);
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;
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);
// Low bitrate stream
if (includeBaselineStream)
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 url.Replace(
"videobitrate=" + oldValue.ToString(UsCulture),
"videobitrate=" + newValue.ToString(UsCulture),
StringComparison.OrdinalIgnoreCase);
}
return result;
private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder)
{
var selectedIndex = state.SubtitleStream == null ? (int?)null : state.SubtitleStream.Index;
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);
}
}
public object Get(GetBaselineHlsVideoStream request)
private bool EnableAdaptiveBitrateStreaming(StreamState state)
{
var result = GetPlaylistAsync(request, "baseline").Result;
var request = state.Request as GetMasterHlsVideoStream;
return result;
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;
@ -52,23 +54,23 @@ namespace MediaBrowser.Api.Playback.Progressive
if (isVideoRequest)
{
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
{
return GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest).Result;
}
finally
using (state)
{
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 StartFfMpeg(state, outputPath, new CancellationTokenSource()).ConfigureAwait(false);
}
else
await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
try
{
ApiEntryPoint.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
state.Dispose();
}
if (!File.Exists(outputPath))
{
await StartFfMpeg(state, outputPath, cancellationTokenSource).ConfigureAwait(false);
}
else
{
ApiEntryPoint.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
state.Dispose();
}
var result = new ProgressiveStreamWriter(outputPath, Logger, FileSystem);
var job = ApiEntryPoint.Instance.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
var result = new ProgressiveStreamWriter(outputPath, Logger, FileSystem, job);
result.Options["Content-Type"] = contentType;
result.Options["Content-Type"] = contentType;
// Add the response headers to the result object
foreach (var item in responseHeaders)
// Add the response headers to the result object
foreach (var item in responseHeaders)
{
result.Options[item.Key] = item.Value;
}
return result;
}
finally
{
result.Options[item.Key] = item.Value;
ApiEntryPoint.Instance.TranscodingStartLock.Release();
}
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
{
@ -33,7 +34,7 @@ namespace MediaBrowser.Api.Playback
/// <value>The start time ticks.</value>
[ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public long? StartTimeTicks { get; set; }
/// <summary>
/// Gets or sets the audio bit rate.
/// </summary>
@ -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))

@ -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();
name = commandType.ToString();
}
var command = new GeneralCommand
{
Name = commandType.ToString(),
ControllingUserId = currentSession.UserId.HasValue ? currentSession.UserId.Value.ToString("N") : null
};
var currentSession = GetSession();
var task = _sessionManager.SendGeneralCommand(currentSession.Id, request.Id, command, CancellationToken.None);
var command = new GeneralCommand
{
Name = name,
ControllingUserId = currentSession.UserId.HasValue ? currentSession.UserId.Value.ToString("N") : null
};
Task.WaitAll(task);
}
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,37 +9,17 @@ 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).")]
public class GetSubtitle
{
/// <summary>
/// Gets or sets the id.
/// </summary>
/// <value>The id.</value>
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string Id { get; set; }
[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; }
[ApiMember(Name = "Format", Description = "Format", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string Format { get; set; }
[ApiMember(Name = "StartPositionTicks", Description = "StartPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public long StartPositionTicks { get; set; }
}
[Route("/Videos/{Id}/Subtitles/{Index}", "DELETE", Summary = "Deletes an external subtitle file")]
[Authenticated]
public class DeleteSubtitle
{
/// <summary>
@ -53,6 +34,7 @@ namespace MediaBrowser.Api.Library
}
[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")]
@ -63,6 +45,7 @@ namespace MediaBrowser.Api.Library
}
[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")]
@ -70,6 +53,7 @@ namespace MediaBrowser.Api.Library
}
[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")]
@ -80,12 +64,60 @@ namespace MediaBrowser.Api.Library
}
[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>
/// Gets or sets the id.
/// </summary>
/// <value>The id.</value>
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string Id { get; set; }
[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; }
[ApiMember(Name = "Format", Description = "Format", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string Format { get; set; }
[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}/{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 = "GET")]
public string Id { get; set; }
[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; }
[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
{
private readonly ILibraryManager _libraryManager;
@ -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));
@ -129,14 +204,24 @@ namespace MediaBrowser.Api.Library
private async Task<Stream> GetSubtitles(GetSubtitle request)
{
return await _subtitleEncoder.GetSubtitles(request.Id,
request.MediaSourceId,
request.Index,
return await _subtitleEncoder.GetSubtitles(request.Id,
request.MediaSourceId,
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);

@ -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);
}
}
}

@ -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);
}
}
}

@ -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

@ -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>

@ -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;
@ -188,260 +186,97 @@ 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
/// Class GetLocalTrailers
/// </summary>
[Route("/Users/{UserId}/PlayingItems/{Id}", "POST")]
[Api(Description = "Reports that a user has begun playing an item")]
public class OnPlaybackStart : IReturnVoid
[Route("/Users/{UserId}/Items/{Id}/LocalTrailers", "GET")]
[Api(Description = "Gets local trailers for an item")]
public class GetLocalTrailers : IReturn<List<BaseItemDto>>
{
/// <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")]
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
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")]
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
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
/// Class GetSpecialFeatures
/// </summary>
[Route("/Users/{UserId}/PlayingItems/{Id}/Progress", "POST")]
[Api(Description = "Reports a user's playback progress")]
public class OnPlaybackProgress : IReturnVoid
[Route("/Users/{UserId}/Items/{Id}/SpecialFeatures", "GET")]
[Api(Description = "Gets special features for an item")]
public class GetSpecialFeatures : IReturn<List<BaseItemDto>>
{
/// <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")]
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
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")]
[ApiMember(Name = "Id", Description = "Movie Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
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
[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 = "DELETE")]
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
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 = "Limit", Description = "Limit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int Limit { get; set; }
[ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
public string MediaSourceId { 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; }
/// <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; }
}
[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; }
/// <summary>
/// Class GetLocalTrailers
/// </summary>
[Route("/Users/{UserId}/Items/{Id}/LocalTrailers", "GET")]
[Api(Description = "Gets local trailers for an item")]
public class GetLocalTrailers : IReturn<List<BaseItemDto>>
{
/// <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 = "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; }
/// <summary>
/// Gets or sets the id.
/// </summary>
/// <value>The id.</value>
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string Id { 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; }
/// <summary>
/// Class GetSpecialFeatures
/// </summary>
[Route("/Users/{UserId}/Items/{Id}/SpecialFeatures", "GET")]
[Api(Description = "Gets special features for an item")]
public class GetSpecialFeatures : IReturn<List<BaseItemDto>>
{
/// <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 = "IsPlayed", Description = "Filter by items that are played, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool? IsPlayed { get; set; }
/// <summary>
/// Gets or sets the id.
/// </summary>
/// <value>The id.</value>
[ApiMember(Name = "Id", Description = "Movie Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string Id { 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>
@ -266,67 +300,54 @@ namespace MediaBrowser.Api
/// <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));
var user = _userManager.GetUserById(request.Id);
if (user == null)
{
throw new ArgumentException(string.Format("User {0} not found.", request.Username));
throw new ResourceNotFoundException("User not found");
}
var result = AuthenticateUser(new AuthenticateUser { Id = user.Id, Password = request.Password }).Result;
return ToOptimizedResult(result);
return Post(new AuthenticateUserByName
{
Username = user.Name,
Password = request.Password
});
}
private async Task<AuthenticationResult> AuthenticateUser(AuthenticateUser request)
public async Task<object> Post(AuthenticateUserByName request)
{
var user = _userManager.GetUserById(request.Id);
var auth = AuthorizationContext.GetAuthorizationInfo(Request);
if (user == null)
if (string.IsNullOrWhiteSpace(auth.Client))
{
throw new ResourceNotFoundException("User not found");
auth.Client = "Unknown app";
}
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))
if (string.IsNullOrWhiteSpace(auth.Device))
{
var success = await _userManager.AuthenticateUser(user, request.Password).ConfigureAwait(false);
if (!success)
{
// Unauthorized
throw new UnauthorizedAccessException("Invalid user or password entered.");
}
return new AuthenticationResult
{
User = _dtoService.GetUserDto(user)
};
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
}, Request.IsLocal).ConfigureAwait(false);
return result;
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,30 +371,33 @@ 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);
}
}
/// <summary>
/// Posts the specified request.
/// </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);
}
@ -144,8 +164,8 @@ namespace MediaBrowser.Common.Implementations.Configuration
/// </summary>
private void UpdateCachePath()
{
((BaseApplicationPaths)CommonApplicationPaths).CachePath = string.IsNullOrEmpty(CommonConfiguration.CachePath) ?
null :
((BaseApplicationPaths)CommonApplicationPaths).CachePath = string.IsNullOrEmpty(CommonConfiguration.CachePath) ?
null :
CommonConfiguration.CachePath;
}
@ -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
@ -135,4 +135,4 @@ xcopy "$(TargetPath)" "$(SolutionDir)\Nuget\dlls\" /y /d /r /i
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>

@ -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())
@ -63,7 +146,7 @@ namespace MediaBrowser.Common.Implementations.Networking
.Select(i => i.ToString())
.Reverse();
}
/// <summary>
/// Gets a random port number that is currently available
/// </summary>

@ -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,17 +177,27 @@ 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)
{
return _lastPackageListResult.Item1;
}
}
using (var json = await _httpClient.Get(Constants.Constants.MbAdminUrl + "service/MB3Packages.json", cancellationToken).ConfigureAwait(false))
{
cancellationToken.ThrowIfCancellationRequested();
@ -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)
@ -264,7 +284,7 @@ namespace MediaBrowser.Common.Implementations.Updates
{
var packages = await GetAvailablePackages(CancellationToken.None).ConfigureAwait(false);
var package = packages.FirstOrDefault(p => string.Equals(p.guid, guid ?? "none", StringComparison.OrdinalIgnoreCase))
var package = packages.FirstOrDefault(p => string.Equals(p.guid, guid ?? "none", StringComparison.OrdinalIgnoreCase))
?? packages.FirstOrDefault(p => p.name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (package == null)
@ -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>
@ -300,7 +320,7 @@ namespace MediaBrowser.Common.Implementations.Updates
/// <returns>PackageVersionInfo.</returns>
public PackageVersionInfo GetLatestCompatibleVersion(IEnumerable<PackageInfo> availablePackages, string name, string guid, Version currentServerVersion, PackageVersionClass classification = PackageVersionClass.Release)
{
var package = availablePackages.FirstOrDefault(p => string.Equals(p.guid, guid ?? "none", StringComparison.OrdinalIgnoreCase))
var package = availablePackages.FirstOrDefault(p => string.Equals(p.guid, guid ?? "none", StringComparison.OrdinalIgnoreCase))
?? availablePackages.FirstOrDefault(p => p.name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (package == null)
@ -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,21 +36,25 @@ 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));
// Save it after load in case we got new items
File.WriteAllBytes(path, newBytes);
}
xmlSerializer.SerializeToStream(configuration, stream);
return configuration;
}
// 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;
}
}
/// <summary>
/// Reads an xml configuration file from the file system

@ -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; }
}
}

@ -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
@ -128,4 +129,4 @@ xcopy "$(TargetPath)" "$(SolutionDir)\Nuget\dlls\" /y /d /r /i
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save