You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
jellyfin/MediaBrowser.WebDashboard/Api/DashboardService.cs

666 lines
27 KiB

using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.IO;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.ScheduledTasks;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Tasks;
using ServiceStack;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using ServiceStack.Web;
namespace MediaBrowser.WebDashboard.Api
{
/// <summary>
/// Class GetDashboardConfigurationPages
/// </summary>
[Route("/dashboard/ConfigurationPages", "GET")]
public class GetDashboardConfigurationPages : IReturn<List<ConfigurationPageInfo>>
{
/// <summary>
/// Gets or sets the type of the page.
/// </summary>
/// <value>The type of the page.</value>
public ConfigurationPageType? PageType { get; set; }
}
/// <summary>
/// Class GetDashboardConfigurationPage
/// </summary>
[Route("/dashboard/ConfigurationPage", "GET")]
public class GetDashboardConfigurationPage
{
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
public string Name { get; set; }
}
/// <summary>
/// Class GetDashboardResource
/// </summary>
[Route("/dashboard/{ResourceName*}", "GET")]
public class GetDashboardResource
{
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
public string ResourceName { get; set; }
/// <summary>
/// Gets or sets the V.
/// </summary>
/// <value>The V.</value>
public string V { get; set; }
}
/// <summary>
/// Class GetDashboardInfo
/// </summary>
[Route("/dashboard/dashboardInfo", "GET")]
public class GetDashboardInfo : IReturn<DashboardInfo>
{
}
/// <summary>
/// Class DashboardService
/// </summary>
public class DashboardService : IRestfulService, IHasResultFactory
{
/// <summary>
/// Gets or sets the logger.
/// </summary>
/// <value>The logger.</value>
public ILogger Logger { get; set; }
/// <summary>
/// Gets or sets the HTTP result factory.
/// </summary>
/// <value>The HTTP result factory.</value>
public IHttpResultFactory ResultFactory { get; set; }
/// <summary>
/// Gets or sets the request context.
/// </summary>
/// <value>The request context.</value>
public IRequest Request { get; set; }
/// <summary>
/// Gets or sets the task manager.
/// </summary>
/// <value>The task manager.</value>
private readonly ITaskManager _taskManager;
/// <summary>
/// The _app host
/// </summary>
private readonly IServerApplicationHost _appHost;
/// <summary>
/// The _server configuration manager
/// </summary>
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly ISessionManager _sessionManager;
11 years ago
private readonly IDtoService _dtoService;
private readonly IFileSystem _fileSystem;
/// <summary>
/// Initializes a new instance of the <see cref="DashboardService" /> class.
/// </summary>
/// <param name="taskManager">The task manager.</param>
/// <param name="appHost">The app host.</param>
/// <param name="serverConfigurationManager">The server configuration manager.</param>
/// <param name="sessionManager">The session manager.</param>
public DashboardService(ITaskManager taskManager, IServerApplicationHost appHost, IServerConfigurationManager serverConfigurationManager, ISessionManager sessionManager, IDtoService dtoService, IFileSystem fileSystem)
{
_taskManager = taskManager;
_appHost = appHost;
_serverConfigurationManager = serverConfigurationManager;
_sessionManager = sessionManager;
11 years ago
_dtoService = dtoService;
_fileSystem = fileSystem;
}
/// <summary>
/// Gets the dashboard UI path.
/// </summary>
/// <value>The dashboard UI path.</value>
public string DashboardUIPath
{
get
{
if (!string.IsNullOrEmpty(_serverConfigurationManager.Configuration.DashboardSourcePath))
{
return _serverConfigurationManager.Configuration.DashboardSourcePath;
}
var runningDirectory = Path.GetDirectoryName(_serverConfigurationManager.ApplicationPaths.ApplicationPath);
return Path.Combine(runningDirectory, "dashboard-ui");
}
}
/// <summary>
/// Gets the dashboard resource path.
/// </summary>
/// <param name="virtualPath">The virtual path.</param>
/// <returns>System.String.</returns>
private string GetDashboardResourcePath(string virtualPath)
{
return Path.Combine(DashboardUIPath, virtualPath.Replace('/', Path.DirectorySeparatorChar));
}
/// <summary>
/// Gets the specified request.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.Object.</returns>
public object Get(GetDashboardInfo request)
{
11 years ago
var result = GetDashboardInfo(_appHost, _taskManager, _sessionManager, _dtoService);
return ResultFactory.GetOptimizedResult(Request, result);
}
/// <summary>
/// Gets the dashboard info.
/// </summary>
/// <param name="appHost">The app host.</param>
/// <param name="taskManager">The task manager.</param>
/// <param name="connectionManager">The connection manager.</param>
/// <returns>DashboardInfo.</returns>
public static DashboardInfo GetDashboardInfo(IServerApplicationHost appHost,
ITaskManager taskManager,
11 years ago
ISessionManager connectionManager, IDtoService dtoService)
{
var connections = connectionManager.Sessions.Where(i => i.IsActive).ToList();
return new DashboardInfo
{
SystemInfo = appHost.GetSystemInfo(),
RunningTasks = taskManager.ScheduledTasks.Where(i => i.State == TaskState.Running || i.State == TaskState.Cancelling)
.Select(ScheduledTaskHelpers.GetTaskInfo)
.ToList(),
ApplicationUpdateTaskId = taskManager.ScheduledTasks.First(t => t.ScheduledTask.GetType().Name.Equals("SystemUpdateTask", StringComparison.OrdinalIgnoreCase)).Id,
ActiveConnections = connections.Select(dtoService.GetSessionInfoDto).ToList()
};
}
/// <summary>
/// Gets the specified request.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.Object.</returns>
public object Get(GetDashboardConfigurationPage request)
{
var page = ServerEntryPoint.Instance.PluginConfigurationPages.First(p => p.Name.Equals(request.Name, StringComparison.OrdinalIgnoreCase));
return ResultFactory.GetStaticResult(Request, page.Plugin.Version.ToString().GetMD5(), page.Plugin.AssemblyDateLastModified, null, MimeTypes.GetMimeType("page.html"), () => ModifyHtml(page.GetHtmlStream()));
}
/// <summary>
/// Gets the specified request.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.Object.</returns>
public object Get(GetDashboardConfigurationPages request)
{
const string unavilableMessage = "The server is still loading. Please try again momentarily.";
var instance = ServerEntryPoint.Instance;
if (instance == null)
{
throw new InvalidOperationException(unavilableMessage);
}
var pages = instance.PluginConfigurationPages;
if (pages == null)
{
throw new InvalidOperationException(unavilableMessage);
}
if (request.PageType.HasValue)
{
pages = pages.Where(p => p.ConfigurationPageType == request.PageType.Value);
}
// Don't allow a failing plugin to fail them all
var configPages = pages.Select(p =>
{
try
{
return new ConfigurationPageInfo(p);
}
catch (Exception ex)
{
Logger.ErrorException("Error getting plugin information from {0}", ex, p.GetType().Name);
return null;
}
})
.Where(i => i != null)
.ToList();
return ResultFactory.GetOptimizedResult(Request, configPages);
}
/// <summary>
/// Gets the specified request.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.Object.</returns>
public object Get(GetDashboardResource request)
{
var path = request.ResourceName;
var contentType = MimeTypes.GetMimeType(path);
// Don't cache if not configured to do so
// But always cache images to simulate production
11 years ago
if (!_serverConfigurationManager.Configuration.EnableDashboardResponseCaching &&
!contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) &&
!contentType.StartsWith("font/", StringComparison.OrdinalIgnoreCase))
{
return ResultFactory.GetResult(GetResourceStream(path).Result, contentType);
}
TimeSpan? cacheDuration = null;
// Cache images unconditionally - updates to image files will require new filename
// If there's a version number in the query string we can cache this unconditionally
11 years ago
if (contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) || contentType.StartsWith("font/", StringComparison.OrdinalIgnoreCase) || !string.IsNullOrEmpty(request.V))
{
cacheDuration = TimeSpan.FromDays(365);
}
var assembly = GetType().Assembly.GetName();
var cacheKey = (assembly.Version + path).GetMD5();
return ResultFactory.GetStaticResult(Request, cacheKey, null, cacheDuration, contentType, () => GetResourceStream(path));
}
/// <summary>
/// Gets the resource stream.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>Task{Stream}.</returns>
private async Task<Stream> GetResourceStream(string path)
{
Stream resourceStream;
if (path.Equals("scripts/all.js", StringComparison.OrdinalIgnoreCase))
{
resourceStream = await GetAllJavascript().ConfigureAwait(false);
}
else if (path.Equals("css/all.css", StringComparison.OrdinalIgnoreCase))
{
resourceStream = await GetAllCss().ConfigureAwait(false);
}
else
{
resourceStream = GetRawResourceStream(path);
}
if (resourceStream != null)
{
var isHtml = IsHtml(path);
// Don't apply any caching for html pages
// jQuery ajax doesn't seem to handle if-modified-since correctly
if (isHtml)
{
resourceStream = await ModifyHtml(resourceStream).ConfigureAwait(false);
}
}
return resourceStream;
}
/// <summary>
/// Gets the raw resource stream.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>Task{Stream}.</returns>
private Stream GetRawResourceStream(string path)
{
return _fileSystem.GetFileStream(GetDashboardResourcePath(path), FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
}
/// <summary>
/// Determines whether the specified path is HTML.
/// </summary>
/// <param name="path">The path.</param>
/// <returns><c>true</c> if the specified path is HTML; otherwise, <c>false</c>.</returns>
private bool IsHtml(string path)
{
return Path.GetExtension(path).EndsWith("html", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Modifies the HTML by adding common meta tags, css and js.
/// </summary>
/// <param name="sourceStream">The source stream.</param>
/// <returns>Task{Stream}.</returns>
internal async Task<Stream> ModifyHtml(Stream sourceStream)
{
string html;
using (var memoryStream = new MemoryStream())
{
await sourceStream.CopyToAsync(memoryStream).ConfigureAwait(false);
html = Encoding.UTF8.GetString(memoryStream.ToArray());
}
var version = GetType().Assembly.GetName().Version;
html = html.Replace("<head>", "<head>" + GetMetaTags() + GetCommonCss(version) + GetCommonJavascript(version));
var bytes = Encoding.UTF8.GetBytes(html);
sourceStream.Dispose();
return new MemoryStream(bytes);
}
/// <summary>
/// Gets the meta tags.
/// </summary>
/// <returns>System.String.</returns>
private static string GetMetaTags()
{
var sb = new StringBuilder();
sb.Append("<meta http-equiv=\"X-UA-Compatibility\" content=\"IE=Edge\">");
sb.Append("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">");
sb.Append("<meta name=\"apple-mobile-web-app-capable\" content=\"yes\">");
//sb.Append("<meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">");
// http://developer.apple.com/library/ios/#DOCUMENTATION/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html
sb.Append("<link rel=\"apple-touch-icon\" href=\"css/images/touchicon.png\" />");
sb.Append("<link rel=\"apple-touch-icon\" sizes=\"72x72\" href=\"css/images/touchicon72.png\" />");
sb.Append("<link rel=\"apple-touch-icon\" sizes=\"114x114\" href=\"css/images/touchicon114.png\" />");
sb.Append("<link rel=\"apple-touch-startup-image\" href=\"css/images/iossplash.png\" />");
sb.Append("<link rel=\"shortcut icon\" href=\"favicon.ico\" />");
return sb.ToString();
}
/// <summary>
/// Gets the common CSS.
/// </summary>
/// <param name="version">The version.</param>
/// <returns>System.String.</returns>
private static string GetCommonCss(Version version)
{
var versionString = "?v=" + version;
var files = new[]
{
"thirdparty/jquerymobile-1.4.0/jquery.mobile-1.4.0.min.css",
"css/all.css" + versionString
};
var tags = files.Select(s => string.Format("<link rel=\"stylesheet\" href=\"{0}\" />", s)).ToArray();
return string.Join(string.Empty, tags);
}
/// <summary>
/// Gets the common javascript.
/// </summary>
/// <param name="version">The version.</param>
/// <returns>System.String.</returns>
private static string GetCommonJavascript(Version version)
{
var builder = new StringBuilder();
builder.Append("<script type=\"text/javascript\">if (navigator.userAgent.toLowerCase().indexOf('compatible; msie 7')!=-1){alert(\"Please ensure you're running at least IE10 and that compatibility mode is disabled.\");}");
builder.Append("</script>");
var versionString = "?v=" + version;
var files = new[]
{
"scripts/all.js" + versionString,
"thirdparty/jstree1.0/jquery.jstree.min.js"
};
var tags = files.Select(s => string.Format("<script src=\"{0}\"></script>", s)).ToArray();
builder.Append(string.Join(string.Empty, tags));
return builder.ToString();
}
/// <summary>
/// Gets a stream containing all concatenated javascript
/// </summary>
/// <returns>Task{Stream}.</returns>
private async Task<Stream> GetAllJavascript()
{
var assembly = GetType().Assembly;
var scriptFiles = new[]
{
"extensions.js",
"site.js",
"librarybrowser.js",
"librarymenu.js",
"ratingdialog.js",
"aboutpage.js",
"allusersettings.js",
"alphapicker.js",
"addpluginpage.js",
"advancedconfigurationpage.js",
"metadataadvanced.js",
"boxsets.js",
"appsplayback.js",
"appsweather.js",
"dashboardpage.js",
"directorybrowser.js",
"edititemmetadata.js",
"edititempeople.js",
"edititemimages.js",
"gamesrecommendedpage.js",
"gamesystemspage.js",
"gamespage.js",
"gamegenrepage.js",
"gamestudiospage.js",
"indexpage.js",
12 years ago
"itembynamedetailpage.js",
"itemdetailpage.js",
"itemgallery.js",
"itemlistpage.js",
"librarysettings.js",
"livetvchannel.js",
"livetvchannels.js",
"livetvguide.js",
"livetvnewrecording.js",
11 years ago
"livetvprogram.js",
"livetvrecording.js",
"livetvrecordings.js",
"livetvtimer.js",
"livetvseriestimer.js",
"livetvseriestimers.js",
"livetvtimers.js",
"loginpage.js",
"logpage.js",
"medialibrarypage.js",
"mediaplayer.js",
"metadataconfigurationpage.js",
"metadataimagespage.js",
"moviegenres.js",
"movies.js",
"moviepeople.js",
"moviesrecommended.js",
"moviestudios.js",
"movietrailers.js",
12 years ago
"musicalbums.js",
"musicalbumartists.js",
12 years ago
"musicartists.js",
"musicgenres.js",
"musicrecommended.js",
"musicvideos.js",
"notifications.js",
"playlist.js",
"plugincatalogpage.js",
"pluginspage.js",
"pluginupdatespage.js",
"remotecontrol.js",
"scheduledtaskpage.js",
"scheduledtaskspage.js",
"search.js",
"songs.js",
"supporterkeypage.js",
"supporterpage.js",
"episodes.js",
"tvgenres.js",
"tvnextup.js",
"tvpeople.js",
"tvrecommended.js",
"tvshows.js",
"tvstudios.js",
"tvupcoming.js",
"useredit.js",
"userpassword.js",
"userimagepage.js",
"userprofilespage.js",
"usersettings.js",
"userparentalcontrol.js",
"wizardfinishpage.js",
11 years ago
"wizardimagesettings.js",
"wizardservice.js",
"wizardstartpage.js",
"wizardsettings.js",
"wizarduserpage.js"
};
var memoryStream = new MemoryStream();
var newLineBytes = Encoding.UTF8.GetBytes(Environment.NewLine);
await AppendResource(memoryStream, "thirdparty/jquery-2.0.3.min.js", newLineBytes).ConfigureAwait(false);
await AppendResource(memoryStream, "thirdparty/jquerymobile-1.4.0/jquery.mobile-1.4.0.min.js", newLineBytes).ConfigureAwait(false);
//await AppendResource(memoryStream, "thirdparty/jquery.infinite-scroll-helper.min.js", newLineBytes).ConfigureAwait(false);
var versionString = string.Format("window.dashboardVersion='{0}';", _appHost.ApplicationVersion);
var versionBytes = Encoding.UTF8.GetBytes(versionString);
await memoryStream.WriteAsync(versionBytes, 0, versionBytes.Length).ConfigureAwait(false);
await memoryStream.WriteAsync(newLineBytes, 0, newLineBytes.Length).ConfigureAwait(false);
await AppendResource(memoryStream, "thirdparty/autonumeric/autoNumeric.min.js", newLineBytes).ConfigureAwait(false);
await AppendResource(assembly, memoryStream, "MediaBrowser.WebDashboard.ApiClient.js", newLineBytes).ConfigureAwait(false);
foreach (var file in scriptFiles)
{
await AppendResource(memoryStream, "scripts/" + file, newLineBytes).ConfigureAwait(false);
}
memoryStream.Position = 0;
return memoryStream;
}
/// <summary>
/// Gets all CSS.
/// </summary>
/// <returns>Task{Stream}.</returns>
private async Task<Stream> GetAllCss()
{
var files = new[]
{
"site.css",
"mediaplayer.css",
"librarybrowser.css",
"detailtable.css",
"posteritem.css",
"tileitem.css",
"metadataeditor.css",
"notifications.css",
"search.css",
"pluginupdates.css",
"remotecontrol.css",
11 years ago
"userimage.css",
"livetv.css",
"icons.css"
};
var memoryStream = new MemoryStream();
var newLineBytes = Encoding.UTF8.GetBytes(Environment.NewLine);
foreach (var file in files)
{
await AppendResource(memoryStream, "css/" + file, newLineBytes).ConfigureAwait(false);
}
memoryStream.Position = 0;
return memoryStream;
}
/// <summary>
/// Appends the resource.
/// </summary>
/// <param name="assembly">The assembly.</param>
/// <param name="outputStream">The output stream.</param>
/// <param name="path">The path.</param>
/// <param name="newLineBytes">The new line bytes.</param>
/// <returns>Task.</returns>
private async Task AppendResource(Assembly assembly, Stream outputStream, string path, byte[] newLineBytes)
{
using (var stream = assembly.GetManifestResourceStream(path))
{
await stream.CopyToAsync(outputStream).ConfigureAwait(false);
await outputStream.WriteAsync(newLineBytes, 0, newLineBytes.Length).ConfigureAwait(false);
}
}
/// <summary>
/// Appends the resource.
/// </summary>
/// <param name="outputStream">The output stream.</param>
/// <param name="path">The path.</param>
/// <param name="newLineBytes">The new line bytes.</param>
/// <returns>Task.</returns>
private async Task AppendResource(Stream outputStream, string path, byte[] newLineBytes)
{
path = GetDashboardResourcePath(path);
using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
{
using (var streamReader = new StreamReader(fs))
{
var text = await streamReader.ReadToEndAsync().ConfigureAwait(false);
var bytes = Encoding.UTF8.GetBytes(text);
await outputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
}
}
await outputStream.WriteAsync(newLineBytes, 0, newLineBytes.Length).ConfigureAwait(false);
}
}
}