diff options
29 files changed, 846 insertions, 939 deletions
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 5772dd479..25ee7e9ec 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -97,7 +97,6 @@ using MediaBrowser.Providers.Chapters; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.TheTvdb; using MediaBrowser.Providers.Subtitles; -using MediaBrowser.WebDashboard.Api; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -1037,9 +1036,6 @@ namespace Emby.Server.Implementations // Include composable parts in the Api assembly yield return typeof(ApiEntryPoint).Assembly; - // Include composable parts in the Dashboard assembly - yield return typeof(DashboardService).Assembly; - // Include composable parts in the Model assembly yield return typeof(SystemInfo).Assembly; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index e71e437ac..5272e2692 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -13,7 +13,6 @@ <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> <ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" /> - <ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj" /> <ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" /> <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" /> <ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" /> diff --git a/Jellyfin.Api/Controllers/BrandingController.cs b/Jellyfin.Api/Controllers/BrandingController.cs new file mode 100644 index 000000000..67790c0e4 --- /dev/null +++ b/Jellyfin.Api/Controllers/BrandingController.cs @@ -0,0 +1,57 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.Branding; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// <summary> + /// Branding controller. + /// </summary> + public class BrandingController : BaseJellyfinApiController + { + private readonly IServerConfigurationManager _serverConfigurationManager; + + /// <summary> + /// Initializes a new instance of the <see cref="BrandingController"/> class. + /// </summary> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + public BrandingController(IServerConfigurationManager serverConfigurationManager) + { + _serverConfigurationManager = serverConfigurationManager; + } + + /// <summary> + /// Gets branding configuration. + /// </summary> + /// <response code="200">Branding configuration returned.</response> + /// <returns>An <see cref="OkResult"/> containing the branding configuration.</returns> + [HttpGet("Configuration")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult<BrandingOptions> GetBrandingOptions() + { + return _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); + } + + /// <summary> + /// Gets branding css. + /// </summary> + /// <response code="200">Branding css returned.</response> + /// <response code="204">No branding css configured.</response> + /// <returns> + /// An <see cref="OkResult"/> containing the branding css if exist, + /// or a <see cref="NoContentResult"/> if the css is not configured. + /// </returns> + [HttpGet("Css")] + [HttpGet("Css.css")] + [Produces("text/css")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult<string> GetBrandingCss() + { + var options = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); + return options.CustomCss ?? string.Empty; + } + } +} diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs new file mode 100644 index 000000000..aab920ff3 --- /dev/null +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using Jellyfin.Api.Models; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Plugins; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Controllers +{ + /// <summary> + /// The dashboard controller. + /// </summary> + public class DashboardController : BaseJellyfinApiController + { + private readonly ILogger<DashboardController> _logger; + private readonly IServerApplicationHost _appHost; + private readonly IConfiguration _appConfig; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly IResourceFileManager _resourceFileManager; + + /// <summary> + /// Initializes a new instance of the <see cref="DashboardController"/> class. + /// </summary> + /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param> + /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param> + /// <param name="appConfig">Instance of <see cref="IConfiguration"/> interface.</param> + /// <param name="resourceFileManager">Instance of <see cref="IResourceFileManager"/> interface.</param> + /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param> + public DashboardController( + ILogger<DashboardController> logger, + IServerApplicationHost appHost, + IConfiguration appConfig, + IResourceFileManager resourceFileManager, + IServerConfigurationManager serverConfigurationManager) + { + _logger = logger; + _appHost = appHost; + _appConfig = appConfig; + _resourceFileManager = resourceFileManager; + _serverConfigurationManager = serverConfigurationManager; + } + + /// <summary> + /// Gets the path of the directory containing the static web interface content, or null if the server is not + /// hosting the web client. + /// </summary> + private string? WebClientUiPath => GetWebClientUiPath(_appConfig, _serverConfigurationManager); + + /// <summary> + /// Gets the configuration pages. + /// </summary> + /// <param name="enableInMainMenu">Whether to enable in the main menu.</param> + /// <param name="pageType">The <see cref="ConfigurationPageInfo"/>.</param> + /// <response code="200">ConfigurationPages returned.</response> + /// <response code="404">Server still loading.</response> + /// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns> + [HttpGet("/web/ConfigurationPages")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult<IEnumerable<ConfigurationPageInfo?>> GetConfigurationPages( + [FromQuery] bool? enableInMainMenu, + [FromQuery] ConfigurationPageType? pageType) + { + const string unavailableMessage = "The server is still loading. Please try again momentarily."; + + var pages = _appHost.GetExports<IPluginConfigurationPage>().ToList(); + + if (pages == null) + { + return NotFound(unavailableMessage); + } + + // Don't allow a failing plugin to fail them all + var configPages = pages.Select(p => + { + try + { + return new ConfigurationPageInfo(p); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting plugin information from {Plugin}", p.GetType().Name); + return null; + } + }) + .Where(i => i != null) + .ToList(); + + configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages)); + + if (pageType.HasValue) + { + configPages = configPages.Where(p => p!.ConfigurationPageType == pageType).ToList(); + } + + if (enableInMainMenu.HasValue) + { + configPages = configPages.Where(p => p!.EnableInMainMenu == enableInMainMenu.Value).ToList(); + } + + return configPages; + } + + /// <summary> + /// Gets a dashboard configuration page. + /// </summary> + /// <param name="name">The name of the page.</param> + /// <response code="200">ConfigurationPage returned.</response> + /// <response code="404">Plugin configuration page not found.</response> + /// <returns>The configuration page.</returns> + [HttpGet("/web/ConfigurationPage")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GetDashboardConfigurationPage([FromQuery] string name) + { + IPlugin? plugin = null; + Stream? stream = null; + + var isJs = false; + var isTemplate = false; + + var page = _appHost.GetExports<IPluginConfigurationPage>().FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)); + if (page != null) + { + plugin = page.Plugin; + stream = page.GetHtmlStream(); + } + + if (plugin == null) + { + var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase)); + if (altPage != null) + { + plugin = altPage.Item2; + stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath); + + isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase); + isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal); + } + } + + if (plugin != null && stream != null) + { + if (isJs) + { + return File(stream, MimeTypes.GetMimeType("page.js")); + } + + if (isTemplate) + { + return File(stream, MimeTypes.GetMimeType("page.html")); + } + + return File(stream, MimeTypes.GetMimeType("page.html")); + } + + return NotFound(); + } + + /// <summary> + /// Gets the robots.txt. + /// </summary> + /// <response code="200">Robots.txt returned.</response> + /// <returns>The robots.txt.</returns> + [HttpGet("/robots.txt")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ApiExplorerSettings(IgnoreApi = true)] + public ActionResult GetRobotsTxt() + { + return GetWebClientResource("robots.txt", string.Empty); + } + + /// <summary> + /// Gets a resource from the web client. + /// </summary> + /// <param name="resourceName">The resource name.</param> + /// <param name="v">The v.</param> + /// <response code="200">Web client returned.</response> + /// <response code="404">Server does not host a web client.</response> + /// <returns>The resource.</returns> + [HttpGet("/web/{*resourceName}")] + [ApiExplorerSettings(IgnoreApi = true)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "v", Justification = "Imported from ServiceStack")] + public ActionResult GetWebClientResource( + [FromRoute] string resourceName, + [FromQuery] string? v) + { + if (!_appConfig.HostWebClient() || WebClientUiPath == null) + { + return NotFound("Server does not host a web client."); + } + + var path = resourceName; + var basePath = WebClientUiPath; + + // Bounce them to the startup wizard if it hasn't been completed yet + if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted + && !Request.Path.Value.Contains("wizard", StringComparison.OrdinalIgnoreCase) + && Request.Path.Value.Contains("index", StringComparison.OrdinalIgnoreCase)) + { + return Redirect("index.html?start=wizard#!/wizardstart.html"); + } + + var stream = new FileStream(_resourceFileManager.GetResourcePath(basePath, path), FileMode.Open, FileAccess.Read); + return File(stream, MimeTypes.GetMimeType(path)); + } + + /// <summary> + /// Gets the favicon. + /// </summary> + /// <response code="200">Favicon.ico returned.</response> + /// <returns>The favicon.</returns> + [HttpGet("/favicon.ico")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ApiExplorerSettings(IgnoreApi = true)] + public ActionResult GetFavIcon() + { + return GetWebClientResource("favicon.ico", string.Empty); + } + + /// <summary> + /// Gets the path of the directory containing the static web interface content. + /// </summary> + /// <param name="appConfig">The app configuration.</param> + /// <param name="serverConfigManager">The server configuration manager.</param> + /// <returns>The directory path, or null if the server is not hosting the web client.</returns> + public static string? GetWebClientUiPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager) + { + if (!appConfig.HostWebClient()) + { + return null; + } + + if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath)) + { + return serverConfigManager.Configuration.DashboardSourcePath; + } + + return serverConfigManager.ApplicationPaths.WebPath; + } + + private IEnumerable<ConfigurationPageInfo> GetConfigPages(IPlugin plugin) + { + return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1)); + } + + private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(IPlugin plugin) + { + if (!(plugin is IHasWebPages hasWebPages)) + { + return new List<Tuple<PluginPageInfo, IPlugin>>(); + } + + return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin)); + } + + private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages() + { + return _appHost.Plugins.SelectMany(GetPluginPages); + } + } +} diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 846cd849a..3f946d9d2 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Threading; +using Jellyfin.Api.Constants; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authorization; @@ -13,7 +14,7 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Display Preferences Controller. /// </summary> - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] public class DisplayPreferencesController : BaseJellyfinApiController { private readonly IDisplayPreferencesRepository _displayPreferencesRepository; diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index dc5b0d906..0934a116a 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Jellyfin.Api.Constants; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -18,7 +19,7 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Filters controller. /// </summary> - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] public class FilterController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs index 0e3c32d3c..4800c0608 100644 --- a/Jellyfin.Api/Controllers/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/ImageByNameController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Mime; +using Jellyfin.Api.Constants; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -43,7 +44,7 @@ namespace Jellyfin.Api.Controllers /// <response code="200">Retrieved list of images.</response> /// <returns>An <see cref="OkResult"/> containing the list of images.</returns> [HttpGet("General")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<IEnumerable<ImageByNameInfo>> GetGeneralImages() { @@ -88,7 +89,7 @@ namespace Jellyfin.Api.Controllers /// <response code="200">Retrieved list of images.</response> /// <returns>An <see cref="OkResult"/> containing the list of images.</returns> [HttpGet("Ratings")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<IEnumerable<ImageByNameInfo>> GetRatingImages() { @@ -121,7 +122,7 @@ namespace Jellyfin.Api.Controllers /// <response code="200">Image list retrieved.</response> /// <returns>An <see cref="OkResult"/> containing the list of images.</returns> [HttpGet("MediaInfo")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<IEnumerable<ImageByNameInfo>> GetMediaInfoImages() { diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs index 75cba450f..44709d0ee 100644 --- a/Jellyfin.Api/Controllers/ItemLookupController.cs +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -30,7 +30,7 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Item lookup controller. /// </summary> - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] public class ItemLookupController : BaseJellyfinApiController { private readonly IProviderManager _providerManager; diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs index e527e5410..e6cdf4edb 100644 --- a/Jellyfin.Api/Controllers/ItemRefreshController.cs +++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using Jellyfin.Api.Constants; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; @@ -15,7 +16,7 @@ namespace Jellyfin.Api.Controllers /// </summary> /// [Authenticated] [Route("/Items")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] public class ItemRefreshController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs new file mode 100644 index 000000000..2dc0d2dc7 --- /dev/null +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -0,0 +1,199 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; +using Jellyfin.Api.Models.PlaylistDtos; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Playlists; +using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Jellyfin.Api.Controllers +{ + /// <summary> + /// Playlists controller. + /// </summary> + [Authorize(Policy = Policies.DefaultAuthorization)] + public class PlaylistsController : BaseJellyfinApiController + { + private readonly IPlaylistManager _playlistManager; + private readonly IDtoService _dtoService; + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + + /// <summary> + /// Initializes a new instance of the <see cref="PlaylistsController"/> class. + /// </summary> + /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> + /// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param> + /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> + /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> + public PlaylistsController( + IDtoService dtoService, + IPlaylistManager playlistManager, + IUserManager userManager, + ILibraryManager libraryManager) + { + _dtoService = dtoService; + _playlistManager = playlistManager; + _userManager = userManager; + _libraryManager = libraryManager; + } + + /// <summary> + /// Creates a new playlist. + /// </summary> + /// <param name="createPlaylistRequest">The create playlist payload.</param> + /// <returns> + /// A <see cref="Task" /> that represents the asynchronous operation to create a playlist. + /// The task result contains an <see cref="OkResult"/> indicating success. + /// </returns> + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist( + [FromBody, BindRequired] CreatePlaylistDto createPlaylistRequest) + { + Guid[] idGuidArray = RequestHelpers.GetGuids(createPlaylistRequest.Ids); + var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest + { + Name = createPlaylistRequest.Name, + ItemIdList = idGuidArray, + UserId = createPlaylistRequest.UserId, + MediaType = createPlaylistRequest.MediaType + }).ConfigureAwait(false); + + return result; + } + + /// <summary> + /// Adds items to a playlist. + /// </summary> + /// <param name="playlistId">The playlist id.</param> + /// <param name="ids">Item id, comma delimited.</param> + /// <param name="userId">The userId.</param> + /// <response code="204">Items added to playlist.</response> + /// <returns>An <see cref="NoContentResult"/> on success.</returns> + [HttpPost("{playlistId}/Items")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult AddToPlaylist( + [FromRoute] string playlistId, + [FromQuery] string ids, + [FromQuery] Guid userId) + { + _playlistManager.AddToPlaylist(playlistId, RequestHelpers.GetGuids(ids), userId); + return NoContent(); + } + + /// <summary> + /// Moves a playlist item. + /// </summary> + /// <param name="playlistId">The playlist id.</param> + /// <param name="itemId">The item id.</param> + /// <param name="newIndex">The new index.</param> + /// <response code="204">Item moved to new index.</response> + /// <returns>An <see cref="NoContentResult"/> on success.</returns> + [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult MoveItem( + [FromRoute] string playlistId, + [FromRoute] string itemId, + [FromRoute] int newIndex) + { + _playlistManager.MoveItem(playlistId, itemId, newIndex); + return NoContent(); + } + + /// <summary> + /// Removes items from a playlist. + /// </summary> + /// <param name="playlistId">The playlist id.</param> + /// <param name="entryIds">The item ids, comma delimited.</param> + /// <response code="204">Items removed.</response> + /// <returns>An <see cref="NoContentResult"/> on success.</returns> + [HttpDelete("{playlistId}/Items")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult RemoveFromPlaylist([FromRoute] string playlistId, [FromQuery] string entryIds) + { + _playlistManager.RemoveFromPlaylist(playlistId, RequestHelpers.Split(entryIds, ',', true)); + return NoContent(); + } + + /// <summary> + /// Gets the original items of a playlist. + /// </summary> + /// <param name="playlistId">The playlist id.</param> + /// <param name="userId">User id.</param> + /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> + /// <param name="limit">Optional. The maximum number of records to return.</param> + /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> + /// <param name="enableImages">Optional. Include image information in output.</param> + /// <param name="enableUserData">Optional. Include user data.</param> + /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> + /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> + /// <response code="200">Original playlist returned.</response> + /// <response code="404">Playlist not found.</response> + /// <returns>The original playlist items.</returns> + [HttpGet("{playlistId}/Items")] + public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems( + [FromRoute] Guid playlistId, + [FromRoute] Guid userId, + [FromRoute] int? startIndex, + [FromRoute] int? limit, + [FromRoute] string fields, + [FromRoute] bool? enableImages, + [FromRoute] bool? enableUserData, + [FromRoute] int? imageTypeLimit, + [FromRoute] string enableImageTypes) + { + var playlist = (Playlist)_libraryManager.GetItemById(playlistId); + if (playlist == null) + { + return NotFound(); + } + + var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null; + + var items = playlist.GetManageableItems().ToArray(); + + var count = items.Length; + + if (startIndex.HasValue) + { + items = items.Skip(startIndex.Value).ToArray(); + } + + if (limit.HasValue) + { + items = items.Take(limit.Value).ToArray(); + } + + var dtoOptions = new DtoOptions() + .AddItemFields(fields) + .AddClientFields(Request) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + + var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user); + + for (int index = 0; index < dtos.Count; index++) + { + dtos[index].PlaylistItemId = items[index].Item1.Id; + } + + var result = new QueryResult<BaseItemDto> + { + Items = dtos, + TotalRecordCount = count + }; + + return result; + } + } +} diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index f6036b748..979d40119 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -20,7 +20,7 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Plugins controller. /// </summary> - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] public class PluginsController : BaseJellyfinApiController { private readonly IApplicationHost _appHost; diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index 41b7f98ee..a0d14be7a 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net.Mime; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Constants; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -25,7 +26,7 @@ namespace Jellyfin.Api.Controllers /// Remote Images Controller. /// </summary> [Route("Images")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] public class RemoteImageController : BaseJellyfinApiController { private readonly IProviderManager _providerManager; diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 315bc9728..39da4178d 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; +using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Devices; @@ -57,7 +58,7 @@ namespace Jellyfin.Api.Controllers /// <response code="200">List of sessions returned.</response> /// <returns>An <see cref="IEnumerable{SessionInfo}"/> with the available sessions.</returns> [HttpGet("/Sessions")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<IEnumerable<SessionInfo>> GetSessions( [FromQuery] Guid controllableByUserId, diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 0d57dcc83..c1f417df5 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -72,7 +72,7 @@ namespace Jellyfin.Api.Controllers /// <response code="200">Users returned.</response> /// <returns>An <see cref="IEnumerable{UserDto}"/> containing the users.</returns> [HttpGet] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isGuest", Justification = "Imported from ServiceStack")] public ActionResult<IEnumerable<UserDto>> GetUsers( @@ -237,7 +237,7 @@ namespace Jellyfin.Api.Controllers /// <response code="404">User not found.</response> /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns> [HttpPost("{userId}/Password")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -295,7 +295,7 @@ namespace Jellyfin.Api.Controllers /// <response code="404">User not found.</response> /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns> [HttpPost("{userId}/EasyPassword")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -337,7 +337,7 @@ namespace Jellyfin.Api.Controllers /// <response code="403">User update forbidden.</response> /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure.</returns> [HttpPost("{userId}")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -381,7 +381,7 @@ namespace Jellyfin.Api.Controllers /// <response code="403">User policy update forbidden.</response> /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure..</returns> [HttpPost("{userId}/Policy")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -437,7 +437,7 @@ namespace Jellyfin.Api.Controllers /// <response code="403">User configuration update forbidden.</response> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("{userId}/Configuration")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult UpdateUserConfiguration( diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs new file mode 100644 index 000000000..effe630a9 --- /dev/null +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -0,0 +1,202 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Threading; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// <summary> + /// The videos controller. + /// </summary> + [Route("Videos")] + public class VideosController : BaseJellyfinApiController + { + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IDtoService _dtoService; + + /// <summary> + /// Initializes a new instance of the <see cref="VideosController"/> class. + /// </summary> + /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> + /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> + /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> + public VideosController( + ILibraryManager libraryManager, + IUserManager userManager, + IDtoService dtoService) + { + _libraryManager = libraryManager; + _userManager = userManager; + _dtoService = dtoService; + } + + /// <summary> + /// Gets additional parts for a video. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <param name="userId">Optional. Filter by user id, and attach user data.</param> + /// <response code="200">Additional parts returned.</response> + /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the parts.</returns> + [HttpGet("{itemId}/AdditionalParts")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute] Guid itemId, [FromQuery] Guid userId) + { + var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null; + + var item = itemId.Equals(Guid.Empty) + ? (!userId.Equals(Guid.Empty) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.RootFolder) + : _libraryManager.GetItemById(itemId); + + var dtoOptions = new DtoOptions(); + dtoOptions = dtoOptions.AddClientFields(Request); + + BaseItemDto[] items; + if (item is Video video) + { + items = video.GetAdditionalParts() + .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video)) + .ToArray(); + } + else + { + items = Array.Empty<BaseItemDto>(); + } + + var result = new QueryResult<BaseItemDto> + { + Items = items, + TotalRecordCount = items.Length + }; + + return result; + } + + /// <summary> + /// Removes alternate video sources. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <response code="204">Alternate sources deleted.</response> + /// <response code="404">Video not found.</response> + /// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="NotFoundResult"/> if the video doesn't exist.</returns> + [HttpDelete("{itemId}/AlternateSources")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult DeleteAlternateSources([FromRoute] Guid itemId) + { + var video = (Video)_libraryManager.GetItemById(itemId); + + if (video == null) + { + return NotFound("The video either does not exist or the id does not belong to a video."); + } + + foreach (var link in video.GetLinkedAlternateVersions()) + { + link.SetPrimaryVersionId(null); + link.LinkedAlternateVersions = Array.Empty<LinkedChild>(); + + link.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + } + + video.LinkedAlternateVersions = Array.Empty<LinkedChild>(); + video.SetPrimaryVersionId(null); + video.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + + return NoContent(); + } + + /// <summary> + /// Merges videos into a single record. + /// </summary> + /// <param name="itemIds">Item id list. This allows multiple, comma delimited.</param> + /// <response code="204">Videos merged.</response> + /// <response code="400">Supply at least 2 video ids.</response> + /// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="BadRequestResult"/> if less than two ids were supplied.</returns> + [HttpPost("MergeVersions")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public ActionResult MergeVersions([FromQuery] string itemIds) + { + var items = RequestHelpers.Split(itemIds, ',', true) + .Select(i => _libraryManager.GetItemById(i)) + .OfType<Video>() + .OrderBy(i => i.Id) + .ToList(); + + if (items.Count < 2) + { + return BadRequest("Please supply at least two videos to merge."); + } + + var videosWithVersions = items.Where(i => i.MediaSourceCount > 1).ToList(); + + var primaryVersion = videosWithVersions.FirstOrDefault(); + if (primaryVersion == null) + { + primaryVersion = items + .OrderBy(i => + { + if (i.Video3DFormat.HasValue || i.VideoType != VideoType.VideoFile) + { + return 1; + } + + return 0; + }) + .ThenByDescending(i => i.GetDefaultVideoStream()?.Width ?? 0) + .First(); + } + + var list = primaryVersion.LinkedAlternateVersions.ToList(); + + foreach (var item in items.Where(i => i.Id != primaryVersion.Id)) + { + item.SetPrimaryVersionId(primaryVersion.Id.ToString("N", CultureInfo.InvariantCulture)); + + item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + + list.Add(new LinkedChild + { + Path = item.Path, + ItemId = item.Id + }); + + foreach (var linkedItem in item.LinkedAlternateVersions) + { + if (!list.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase))) + { + list.Add(linkedItem); + } + } + + if (item.LinkedAlternateVersions.Length > 0) + { + item.LinkedAlternateVersions = Array.Empty<LinkedChild>(); + item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + } + } + + primaryVersion.LinkedAlternateVersions = list.ToArray(); + primaryVersion.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + return NoContent(); + } + } +} diff --git a/MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs b/Jellyfin.Api/Models/ConfigurationPageInfo.cs index e49a4be8a..2aa6373aa 100644 --- a/MediaBrowser.WebDashboard/Api/ConfigurationPageInfo.cs +++ b/Jellyfin.Api/Models/ConfigurationPageInfo.cs @@ -1,13 +1,18 @@ -#pragma warning disable CS1591 - -using MediaBrowser.Common.Plugins; +using MediaBrowser.Common.Plugins; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Plugins; -namespace MediaBrowser.WebDashboard.Api +namespace Jellyfin.Api.Models { + /// <summary> + /// The configuration page info. + /// </summary> public class ConfigurationPageInfo { + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationPageInfo"/> class. + /// </summary> + /// <param name="page">Instance of <see cref="IPluginConfigurationPage"/> interface.</param> public ConfigurationPageInfo(IPluginConfigurationPage page) { Name = page.Name; @@ -22,6 +27,11 @@ namespace MediaBrowser.WebDashboard.Api } } + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationPageInfo"/> class. + /// </summary> + /// <param name="plugin">Instance of <see cref="IPlugin"/> interface.</param> + /// <param name="page">Instance of <see cref="PluginPageInfo"/> interface.</param> public ConfigurationPageInfo(IPlugin plugin, PluginPageInfo page) { Name = page.Name; @@ -40,13 +50,25 @@ namespace MediaBrowser.WebDashboard.Api /// <value>The name.</value> public string Name { get; set; } + /// <summary> + /// Gets or sets a value indicating whether the configurations page is enabled in the main menu. + /// </summary> public bool EnableInMainMenu { get; set; } - public string MenuSection { get; set; } + /// <summary> + /// Gets or sets the menu section. + /// </summary> + public string? MenuSection { get; set; } - public string MenuIcon { get; set; } + /// <summary> + /// Gets or sets the menu icon. + /// </summary> + public string? MenuIcon { get; set; } - public string DisplayName { get; set; } + /// <summary> + /// Gets or sets the display name. + /// </summary> + public string? DisplayName { get; set; } /// <summary> /// Gets or sets the type of the configuration page. @@ -58,6 +80,6 @@ namespace MediaBrowser.WebDashboard.Api /// Gets or sets the plugin id. /// </summary> /// <value>The plugin id.</value> - public string PluginId { get; set; } + public string? PluginId { get; set; } } } diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs new file mode 100644 index 000000000..0d67c86f7 --- /dev/null +++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs @@ -0,0 +1,30 @@ +using System; + +namespace Jellyfin.Api.Models.PlaylistDtos +{ + /// <summary> + /// Create new playlist dto. + /// </summary> + public class CreatePlaylistDto + { + /// <summary> + /// Gets or sets the name of the new playlist. + /// </summary> + public string? Name { get; set; } + + /// <summary> + /// Gets or sets item ids to add to the playlist. + /// </summary> + public string? Ids { get; set; } + + /// <summary> + /// Gets or sets the user id. + /// </summary> + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the media type. + /// </summary> + public string? MediaType { get; set; } + } +} diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 3971a08e9..dfc7bbbb1 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -14,9 +14,9 @@ using Emby.Server.Implementations; using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Networking; +using Jellyfin.Api.Controllers; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Extensions; -using MediaBrowser.WebDashboard.Api; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; @@ -172,7 +172,7 @@ namespace Jellyfin.Server // If hosting the web client, validate the client content path if (startupConfig.HostWebClient()) { - string webContentPath = DashboardService.GetDashboardUIPath(startupConfig, appHost.ServerConfigurationManager); + string? webContentPath = DashboardController.GetWebClientUiPath(startupConfig, appHost.ServerConfigurationManager); if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0) { throw new InvalidOperationException( diff --git a/MediaBrowser.Api/BrandingService.cs b/MediaBrowser.Api/BrandingService.cs deleted file mode 100644 index f4724e774..000000000 --- a/MediaBrowser.Api/BrandingService.cs +++ /dev/null @@ -1,44 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Branding; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - [Route("/Branding/Configuration", "GET", Summary = "Gets branding configuration")] - public class GetBrandingOptions : IReturn<BrandingOptions> - { - } - - [Route("/Branding/Css", "GET", Summary = "Gets custom css")] - [Route("/Branding/Css.css", "GET", Summary = "Gets custom css")] - public class GetBrandingCss - { - } - - public class BrandingService : BaseApiService - { - public BrandingService( - ILogger<BrandingService> logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory) - : base(logger, serverConfigurationManager, httpResultFactory) - { - } - - public object Get(GetBrandingOptions request) - { - return ServerConfigurationManager.GetConfiguration<BrandingOptions>("branding"); - } - - public object Get(GetBrandingCss request) - { - var result = ServerConfigurationManager.GetConfiguration<BrandingOptions>("branding"); - - // When null this throws a 405 error under Mono OSX, so default to empty string - return ResultFactory.GetResult(Request, result.CustomCss ?? string.Empty, "text/css"); - } - } -} diff --git a/MediaBrowser.Api/PlaylistService.cs b/MediaBrowser.Api/PlaylistService.cs deleted file mode 100644 index 953b00e35..000000000 --- a/MediaBrowser.Api/PlaylistService.cs +++ /dev/null @@ -1,217 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; -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 MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -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 = "POST")] - public Guid UserId { get; set; } - - [ApiMember(Name = "MediaType", Description = "The playlist media type", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - 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 = "POST")] - public Guid UserId { get; set; } - } - - [Route("/Playlists/{Id}/Items/{ItemId}/Move/{NewIndex}", "POST", Summary = "Moves a playlist item")] - public class MoveItem : IReturnVoid - { - [ApiMember(Name = "ItemId", Description = "ItemId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string ItemId { 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 = "NewIndex", Description = "NewIndex", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public int NewIndex { 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>>, IHasDtoOptions - { - [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public Guid 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 Guid 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, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] - public string Fields { get; set; } - - [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] - public bool? EnableImages { get; set; } - - [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] - public bool? EnableUserData { get; set; } - - [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? ImageTypeLimit { get; set; } - - [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string EnableImageTypes { get; set; } - } - - [Authenticated] - public class PlaylistService : BaseApiService - { - private readonly IPlaylistManager _playlistManager; - private readonly IDtoService _dtoService; - private readonly IUserManager _userManager; - private readonly ILibraryManager _libraryManager; - private readonly IAuthorizationContext _authContext; - - public PlaylistService( - ILogger<PlaylistService> logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IDtoService dtoService, - IPlaylistManager playlistManager, - IUserManager userManager, - ILibraryManager libraryManager, - IAuthorizationContext authContext) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _dtoService = dtoService; - _playlistManager = playlistManager; - _userManager = userManager; - _libraryManager = libraryManager; - _authContext = authContext; - } - - public void Post(MoveItem request) - { - _playlistManager.MoveItem(request.Id, request.ItemId, request.NewIndex); - } - - public async Task<object> Post(CreatePlaylist request) - { - var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest - { - Name = request.Name, - ItemIdList = GetGuids(request.Ids), - UserId = request.UserId, - MediaType = request.MediaType - - }).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public void Post(AddToPlaylist request) - { - _playlistManager.AddToPlaylist(request.Id, GetGuids(request.Ids), request.UserId); - } - - public void Delete(RemoveFromPlaylist request) - { - _playlistManager.RemoveFromPlaylist(request.Id, request.EntryIds.Split(',')); - } - - public object Get(GetPlaylistItems request) - { - var playlist = (Playlist)_libraryManager.GetItemById(request.Id); - var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(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 dtoOptions = GetDtoOptions(_authContext, request); - - var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user); - - for (int index = 0; index < dtos.Count; index++) - { - dtos[index].PlaylistItemId = items[index].Item1.Id; - } - - var result = new QueryResult<BaseItemDto> - { - Items = dtos, - TotalRecordCount = count - }; - - return ToOptimizedResult(result); - } - } -} diff --git a/MediaBrowser.Api/TestService.cs b/MediaBrowser.Api/TestService.cs new file mode 100644 index 000000000..6c999e08d --- /dev/null +++ b/MediaBrowser.Api/TestService.cs @@ -0,0 +1,26 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Api +{ + /// <summary> + /// Service for testing path value. + /// </summary> + public class TestService : BaseApiService + { + /// <summary> + /// Test service. + /// </summary> + /// <param name="logger">Instance of the <see cref="ILogger{TestService}"/> interface.</param> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + /// <param name="httpResultFactory">Instance of the <see cref="IHttpResultFactory"/> interface.</param> + public TestService( + ILogger<TestService> logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory) + : base(logger, serverConfigurationManager, httpResultFactory) + { + } + } +} diff --git a/MediaBrowser.Api/VideosService.cs b/MediaBrowser.Api/VideosService.cs deleted file mode 100644 index 957a279f8..000000000 --- a/MediaBrowser.Api/VideosService.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Threading; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - [Route("/Videos/{Id}/AdditionalParts", "GET", Summary = "Gets additional parts for a video.")] - [Authenticated] - public class GetAdditionalParts : IReturn<QueryResult<BaseItemDto>> - { - [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", 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 = "GET")] - public string Id { get; set; } - } - - [Route("/Videos/{Id}/AlternateSources", "DELETE", Summary = "Removes alternate video sources.")] - [Authenticated(Roles = "Admin")] - public class DeleteAlternateSources : IReturnVoid - { - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public string Id { get; set; } - } - - [Route("/Videos/MergeVersions", "POST", Summary = "Merges videos into a single record")] - [Authenticated(Roles = "Admin")] - public class MergeVersions : IReturnVoid - { - [ApiMember(Name = "Ids", Description = "Item id list. This allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)] - public string Ids { get; set; } - } - - public class VideosService : BaseApiService - { - private readonly ILibraryManager _libraryManager; - private readonly IUserManager _userManager; - private readonly IDtoService _dtoService; - private readonly IAuthorizationContext _authContext; - - public VideosService( - ILogger<VideosService> logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - ILibraryManager libraryManager, - IUserManager userManager, - IDtoService dtoService, - IAuthorizationContext authContext) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _libraryManager = libraryManager; - _userManager = userManager; - _dtoService = dtoService; - _authContext = authContext; - } - - /// <summary> - /// Gets the specified request. - /// </summary> - /// <param name="request">The request.</param> - /// <returns>System.Object.</returns> - public object Get(GetAdditionalParts request) - { - var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null; - - var item = string.IsNullOrEmpty(request.Id) - ? (!request.UserId.Equals(Guid.Empty) - ? _libraryManager.GetUserRootFolder() - : _libraryManager.RootFolder) - : _libraryManager.GetItemById(request.Id); - - var dtoOptions = GetDtoOptions(_authContext, request); - - BaseItemDto[] items; - if (item is Video video) - { - items = video.GetAdditionalParts() - .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video)) - .ToArray(); - } - else - { - items = Array.Empty<BaseItemDto>(); - } - - var result = new QueryResult<BaseItemDto> - { - Items = items, - TotalRecordCount = items.Length - }; - - return ToOptimizedResult(result); - } - - public void Delete(DeleteAlternateSources request) - { - var video = (Video)_libraryManager.GetItemById(request.Id); - - foreach (var link in video.GetLinkedAlternateVersions()) - { - link.SetPrimaryVersionId(null); - link.LinkedAlternateVersions = Array.Empty<LinkedChild>(); - - link.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); - } - - video.LinkedAlternateVersions = Array.Empty<LinkedChild>(); - video.SetPrimaryVersionId(null); - video.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); - } - - public void Post(MergeVersions request) - { - var items = request.Ids.Split(',') - .Select(i => _libraryManager.GetItemById(i)) - .OfType<Video>() - .OrderBy(i => i.Id) - .ToList(); - - if (items.Count < 2) - { - throw new ArgumentException("Please supply at least two videos to merge."); - } - - var videosWithVersions = items.Where(i => i.MediaSourceCount > 1) - .ToList(); - - var primaryVersion = videosWithVersions.FirstOrDefault(); - if (primaryVersion == null) - { - primaryVersion = items.OrderBy(i => - { - if (i.Video3DFormat.HasValue || i.VideoType != Model.Entities.VideoType.VideoFile) - { - return 1; - } - - return 0; - }) - .ThenByDescending(i => - { - return i.GetDefaultVideoStream()?.Width ?? 0; - }).First(); - } - - var list = primaryVersion.LinkedAlternateVersions.ToList(); - - foreach (var item in items.Where(i => i.Id != primaryVersion.Id)) - { - item.SetPrimaryVersionId(primaryVersion.Id.ToString("N", CultureInfo.InvariantCulture)); - - item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); - - list.Add(new LinkedChild - { - Path = item.Path, - ItemId = item.Id - }); - - foreach (var linkedItem in item.LinkedAlternateVersions) - { - if (!list.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase))) - { - list.Add(linkedItem); - } - } - - if (item.LinkedAlternateVersions.Length > 0) - { - item.LinkedAlternateVersions = Array.Empty<LinkedChild>(); - item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); - } - } - - primaryVersion.LinkedAlternateVersions = list.ToArray(); - primaryVersion.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); - } - } -} diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs deleted file mode 100644 index 63cbfd9e4..000000000 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ /dev/null @@ -1,340 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1402 -#pragma warning disable SA1649 - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Plugins; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Extensions; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Plugins; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.WebDashboard.Api -{ - /// <summary> - /// Class GetDashboardConfigurationPages. - /// </summary> - [Route("/web/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; } - - public bool? EnableInMainMenu { get; set; } - } - - /// <summary> - /// Class GetDashboardConfigurationPage. - /// </summary> - [Route("/web/ConfigurationPage", "GET")] - public class GetDashboardConfigurationPage - { - /// <summary> - /// Gets or sets the name. - /// </summary> - /// <value>The name.</value> - public string Name { get; set; } - } - - [Route("/robots.txt", "GET", IsHidden = true)] - public class GetRobotsTxt - { - } - - /// <summary> - /// Class GetDashboardResource. - /// </summary> - [Route("/web/{ResourceName*}", "GET", IsHidden = true)] - 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; } - } - - [Route("/favicon.ico", "GET", IsHidden = true)] - public class GetFavIcon - { - } - - /// <summary> - /// Class DashboardService. - /// </summary> - public class DashboardService : IService, IRequiresRequest - { - /// <summary> - /// Gets or sets the logger. - /// </summary> - /// <value>The logger.</value> - private readonly ILogger<DashboardService> _logger; - - /// <summary> - /// Gets or sets the HTTP result factory. - /// </summary> - /// <value>The HTTP result factory.</value> - private readonly IHttpResultFactory _resultFactory; - private readonly IServerApplicationHost _appHost; - private readonly IConfiguration _appConfig; - private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly IFileSystem _fileSystem; - private readonly IResourceFileManager _resourceFileManager; - - /// <summary> - /// Initializes a new instance of the <see cref="DashboardService" /> class. - /// </summary> - /// <param name="logger">The logger.</param> - /// <param name="appHost">The application host.</param> - /// <param name="appConfig">The application configuration.</param> - /// <param name="resourceFileManager">The resource file manager.</param> - /// <param name="serverConfigurationManager">The server configuration manager.</param> - /// <param name="fileSystem">The file system.</param> - /// <param name="resultFactory">The result factory.</param> - public DashboardService( - ILogger<DashboardService> logger, - IServerApplicationHost appHost, - IConfiguration appConfig, - IResourceFileManager resourceFileManager, - IServerConfigurationManager serverConfigurationManager, - IFileSystem fileSystem, - IHttpResultFactory resultFactory) - { - _logger = logger; - _appHost = appHost; - _appConfig = appConfig; - _resourceFileManager = resourceFileManager; - _serverConfigurationManager = serverConfigurationManager; - _fileSystem = fileSystem; - _resultFactory = resultFactory; - } - - /// <summary> - /// Gets or sets the request context. - /// </summary> - /// <value>The request context.</value> - public IRequest Request { get; set; } - - /// <summary> - /// Gets the path of the directory containing the static web interface content, or null if the server is not - /// hosting the web client. - /// </summary> - public string DashboardUIPath => GetDashboardUIPath(_appConfig, _serverConfigurationManager); - - /// <summary> - /// Gets the path of the directory containing the static web interface content. - /// </summary> - /// <param name="appConfig">The app configuration.</param> - /// <param name="serverConfigManager">The server configuration manager.</param> - /// <returns>The directory path, or null if the server is not hosting the web client.</returns> - public static string GetDashboardUIPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager) - { - if (!appConfig.HostWebClient()) - { - return null; - } - - if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath)) - { - return serverConfigManager.Configuration.DashboardSourcePath; - } - - return serverConfigManager.ApplicationPaths.WebPath; - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Get(GetFavIcon request) - { - return Get(new GetDashboardResource - { - ResourceName = "favicon.ico" - }); - } - - /// <summary> - /// Gets the specified request. - /// </summary> - /// <param name="request">The request.</param> - /// <returns>System.Object.</returns> - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public Task<object> Get(GetDashboardConfigurationPage request) - { - IPlugin plugin = null; - Stream stream = null; - - var isJs = false; - var isTemplate = false; - - var page = ServerEntryPoint.Instance.PluginConfigurationPages.FirstOrDefault(p => string.Equals(p.Name, request.Name, StringComparison.OrdinalIgnoreCase)); - if (page != null) - { - plugin = page.Plugin; - stream = page.GetHtmlStream(); - } - - if (plugin == null) - { - var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, request.Name, StringComparison.OrdinalIgnoreCase)); - if (altPage != null) - { - plugin = altPage.Item2; - stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath); - - isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase); - isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal); - } - } - - if (plugin != null && stream != null) - { - if (isJs) - { - return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.js"), () => Task.FromResult(stream)); - } - - if (isTemplate) - { - return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream)); - } - - return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream)); - } - - throw new ResourceNotFoundException(); - } - - /// <summary> - /// Gets the specified request. - /// </summary> - /// <param name="request">The request.</param> - /// <returns>System.Object.</returns> - public object Get(GetDashboardConfigurationPages request) - { - const string unavailableMessage = "The server is still loading. Please try again momentarily."; - - var instance = ServerEntryPoint.Instance; - - if (instance == null) - { - throw new InvalidOperationException(unavailableMessage); - } - - var pages = instance.PluginConfigurationPages; - - if (pages == null) - { - throw new InvalidOperationException(unavailableMessage); - } - - // Don't allow a failing plugin to fail them all - var configPages = pages.Select(p => - { - try - { - return new ConfigurationPageInfo(p); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting plugin information from {Plugin}", p.GetType().Name); - return null; - } - }) - .Where(i => i != null) - .ToList(); - - configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages)); - - if (request.PageType.HasValue) - { - configPages = configPages.Where(p => p.ConfigurationPageType == request.PageType.Value).ToList(); - } - - if (request.EnableInMainMenu.HasValue) - { - configPages = configPages.Where(p => p.EnableInMainMenu == request.EnableInMainMenu.Value).ToList(); - } - - return configPages; - } - - private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages() - { - return _appHost.Plugins.SelectMany(GetPluginPages); - } - - private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(IPlugin plugin) - { - var hasConfig = plugin as IHasWebPages; - - if (hasConfig == null) - { - return new List<Tuple<PluginPageInfo, IPlugin>>(); - } - - return hasConfig.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin)); - } - - private IEnumerable<ConfigurationPageInfo> GetConfigPages(IPlugin plugin) - { - return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1)); - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Get(GetRobotsTxt request) - { - return Get(new GetDashboardResource - { - ResourceName = "robots.txt" - }); - } - - /// <summary> - /// Gets the specified request. - /// </summary> - /// <param name="request">The request.</param> - /// <returns>System.Object.</returns> - public async Task<object> Get(GetDashboardResource request) - { - if (!_appConfig.HostWebClient() || DashboardUIPath == null) - { - throw new ResourceNotFoundException(); - } - - var path = request?.ResourceName; - var basePath = DashboardUIPath; - - // Bounce them to the startup wizard if it hasn't been completed yet - if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted - && !Request.RawUrl.Contains("wizard", StringComparison.OrdinalIgnoreCase) - && Request.RawUrl.Contains("index", StringComparison.OrdinalIgnoreCase)) - { - Request.Response.Redirect("index.html?start=wizard#!/wizardstart.html"); - return null; - } - - return await _resultFactory.GetStaticFileResult(Request, _resourceFileManager.GetResourcePath(basePath, path)).ConfigureAwait(false); - } - } -} diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj deleted file mode 100644 index bcaee50f2..000000000 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ /dev/null @@ -1,42 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk"> - - <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> - <PropertyGroup> - <ProjectGuid>{5624B7B5-B5A7-41D8-9F10-CC5611109619}</ProjectGuid> - </PropertyGroup> - - <ItemGroup> - <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> - <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> - </ItemGroup> - - <ItemGroup> - <Compile Include="..\SharedVersion.cs" /> - </ItemGroup> - - <ItemGroup> - <None Include="jellyfin-web\**\*.*"> - <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> - </None> - </ItemGroup> - - <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> - <GenerateAssemblyInfo>false</GenerateAssemblyInfo> - <GenerateDocumentationFile>true</GenerateDocumentationFile> - <TreatWarningsAsErrors>true</TreatWarningsAsErrors> - </PropertyGroup> - - <!-- Code Analyzers--> - <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> - <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> - <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> - </ItemGroup> - - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - -</Project> diff --git a/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs b/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs deleted file mode 100644 index 584d49021..000000000 --- a/MediaBrowser.WebDashboard/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Reflection; -using System.Resources; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("MediaBrowser.WebDashboard")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: NeutralResourcesLanguage("en")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] diff --git a/MediaBrowser.WebDashboard/ServerEntryPoint.cs b/MediaBrowser.WebDashboard/ServerEntryPoint.cs deleted file mode 100644 index 5c7e8b3c7..000000000 --- a/MediaBrowser.WebDashboard/ServerEntryPoint.cs +++ /dev/null @@ -1,42 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common; -using MediaBrowser.Controller.Plugins; - -namespace MediaBrowser.WebDashboard -{ - public sealed class ServerEntryPoint : IServerEntryPoint - { - private readonly IApplicationHost _appHost; - - public ServerEntryPoint(IApplicationHost appHost) - { - _appHost = appHost; - Instance = this; - } - - public static ServerEntryPoint Instance { get; private set; } - - /// <summary> - /// Gets the list of plugin configuration pages. - /// </summary> - /// <value>The configuration pages.</value> - public List<IPluginConfigurationPage> PluginConfigurationPages { get; private set; } - - /// <inheritdoc /> - public Task RunAsync() - { - PluginConfigurationPages = _appHost.GetExports<IPluginConfigurationPage>().ToList(); - - return Task.CompletedTask; - } - - /// <inheritdoc /> - public void Dispose() - { - } - } -} diff --git a/MediaBrowser.sln b/MediaBrowser.sln index e100c0b1c..0362eff1c 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -12,8 +12,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Common", "Medi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Model", "MediaBrowser.Model\MediaBrowser.Model.csproj", "{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.WebDashboard", "MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj", "{5624B7B5-B5A7-41D8-9F10-CC5611109619}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Providers", "MediaBrowser.Providers\MediaBrowser.Providers.csproj", "{442B5058-DCAF-4263-BB6A-F21E31120A1B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.XbmcMetadata", "MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj", "{23499896-B135-4527-8574-C26E926EA99E}" @@ -94,10 +92,6 @@ Global {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.Build.0 = Debug|Any CPU {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.ActiveCfg = Release|Any CPU {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|Any CPU.Build.0 = Release|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.Build.0 = Release|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/tests/Jellyfin.Api.Tests/GetPathValueTests.cs b/tests/Jellyfin.Api.Tests/GetPathValueTests.cs index b01d1af1f..397eb2edc 100644 --- a/tests/Jellyfin.Api.Tests/GetPathValueTests.cs +++ b/tests/Jellyfin.Api.Tests/GetPathValueTests.cs @@ -31,8 +31,8 @@ namespace Jellyfin.Api.Tests var confManagerMock = Mock.Of<IServerConfigurationManager>(x => x.Configuration == conf); - var service = new BrandingService( - new NullLogger<BrandingService>(), + var service = new TestService( + new NullLogger<TestService>(), confManagerMock, Mock.Of<IHttpResultFactory>()) { diff --git a/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs b/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs index 34698fe25..5d7f7765c 100644 --- a/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs +++ b/tests/MediaBrowser.Api.Tests/BrandingServiceTests.cs @@ -43,7 +43,7 @@ namespace MediaBrowser.Api.Tests // Assert response.EnsureSuccessStatusCode(); - Assert.Equal("text/css", response.Content.Headers.ContentType.ToString()); + Assert.Equal("text/css; charset=utf-8", response.Content.Headers.ContentType.ToString()); } } } |
