diff options
| author | crobibero <cody@robibe.ro> | 2020-06-23 11:50:42 -0600 |
|---|---|---|
| committer | crobibero <cody@robibe.ro> | 2020-06-23 11:50:42 -0600 |
| commit | b4a773fde80b35959dbaf88f98ceb169862dba84 (patch) | |
| tree | e0c4ef404a2d10bc17bd1055ce39cdaee4d74f71 /Jellyfin.Api | |
| parent | c4f9112b0dae98a2c80d4126ca9dcd82a2271835 (diff) | |
| parent | 57bf8c17d62d58aa0f09c86da2c2a232e20dbbd6 (diff) | |
Merge remote-tracking branch 'upstream/api-migration' into api-livetv
Diffstat (limited to 'Jellyfin.Api')
| -rw-r--r-- | Jellyfin.Api/Controllers/BrandingController.cs | 57 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/DashboardController.cs | 275 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 3 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/FilterController.cs | 3 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/ImageByNameController.cs | 7 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/ItemLookupController.cs | 2 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/ItemRefreshController.cs | 3 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/PlaylistsController.cs | 199 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/PluginsController.cs | 2 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/RemoteImageController.cs | 3 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/SessionController.cs | 3 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/UserController.cs | 12 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/VideosController.cs | 202 | ||||
| -rw-r--r-- | Jellyfin.Api/Models/ConfigurationPageInfo.cs | 85 | ||||
| -rw-r--r-- | Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs | 30 |
15 files changed, 870 insertions, 16 deletions
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/Jellyfin.Api/Models/ConfigurationPageInfo.cs b/Jellyfin.Api/Models/ConfigurationPageInfo.cs new file mode 100644 index 000000000..2aa6373aa --- /dev/null +++ b/Jellyfin.Api/Models/ConfigurationPageInfo.cs @@ -0,0 +1,85 @@ +using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Plugins; + +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; + + ConfigurationPageType = page.ConfigurationPageType; + + if (page.Plugin != null) + { + DisplayName = page.Plugin.Name; + // Don't use "N" because it needs to match Plugin.Id + PluginId = page.Plugin.Id.ToString(); + } + } + + /// <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; + EnableInMainMenu = page.EnableInMainMenu; + MenuSection = page.MenuSection; + MenuIcon = page.MenuIcon; + DisplayName = string.IsNullOrWhiteSpace(page.DisplayName) ? plugin.Name : page.DisplayName; + + // Don't use "N" because it needs to match Plugin.Id + PluginId = plugin.Id.ToString(); + } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <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; } + + /// <summary> + /// Gets or sets the menu section. + /// </summary> + public string? MenuSection { get; set; } + + /// <summary> + /// Gets or sets the menu icon. + /// </summary> + public string? MenuIcon { 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. + /// </summary> + /// <value>The type of the configuration page.</value> + public ConfigurationPageType ConfigurationPageType { get; set; } + + /// <summary> + /// Gets or sets the plugin id. + /// </summary> + /// <value>The plugin id.</value> + 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; } + } +} |
