From 618b893c481a658820370cdf4f62c5a9a2deebd1 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 12 Jun 2020 15:39:06 +0200 Subject: [PATCH 1/3] Move LibraryStructureService to Jellyfin.Api --- .../Controllers/LibraryStructureController.cs | 347 +++++++++++++++ .../Library/LibraryStructureService.cs | 412 ------------------ 2 files changed, 347 insertions(+), 412 deletions(-) create mode 100644 Jellyfin.Api/Controllers/LibraryStructureController.cs delete mode 100644 MediaBrowser.Api/Library/LibraryStructureService.cs diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs new file mode 100644 index 000000000..f074a61db --- /dev/null +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -0,0 +1,347 @@ +#pragma warning disable CA1801 + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using MediaBrowser.Common.Progress; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// The library structure controller. + /// + [Route("/Library/VirtualFolders")] + [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] + public class LibraryStructureController : BaseJellyfinApiController + { + private readonly IServerApplicationPaths _appPaths; + private readonly ILibraryManager _libraryManager; + private readonly ILibraryMonitor _libraryMonitor; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public LibraryStructureController( + IServerConfigurationManager serverConfigurationManager, + ILibraryManager libraryManager, + ILibraryMonitor libraryMonitor) + { + _appPaths = serverConfigurationManager?.ApplicationPaths; + _libraryManager = libraryManager; + _libraryMonitor = libraryMonitor; + } + + /// + /// Gets all virtual folders. + /// + /// The user id. + /// Virtual folders retrieved. + /// An with the virtual folders. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetVirtualFolders([FromQuery] string userId) + { + return _libraryManager.GetVirtualFolders(true); + } + + /// + /// Adds a virtual folder. + /// + /// The name of the virtual folder. + /// The type of the collection. + /// Whether to refresh the library. + /// The paths of the virtual folder. + /// The library options. + /// Folder added. + /// A . + [HttpPost] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult AddVirtualFolder( + [FromQuery] string name, + [FromQuery] string collectionType, + [FromQuery] bool refreshLibrary, + [FromQuery] string[] paths, + [FromQuery] LibraryOptions libraryOptions) + { + libraryOptions ??= new LibraryOptions(); + + if (paths != null && paths.Length > 0) + { + libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo { Path = i }).ToArray(); + } + + _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary); + + return NoContent(); + } + + /// + /// Removes a virtual folder. + /// + /// The name of the folder. + /// Whether to refresh the library. + /// Folder removed. + /// A . + [HttpDelete] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult RemoveVirtualFolder( + [FromQuery] string name, + [FromQuery] bool refreshLibrary) + { + _libraryManager.RemoveVirtualFolder(name, refreshLibrary); + return NoContent(); + } + + /// + /// Renames a virtual folder. + /// + /// The name of the virtual folder. + /// The new name. + /// Whether to refresh the library. + /// Folder renamed. + /// Library doesn't exist. + /// Library already exists. + /// A on success, a if the library doesn't exist, a if the new name is already taken. + /// The new name may not be null. + [HttpPost("Name")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public ActionResult RenameVirtualFolder( + [FromQuery] string name, + [FromQuery] string newName, + [FromQuery] bool refreshLibrary) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrWhiteSpace(newName)) + { + throw new ArgumentNullException(nameof(newName)); + } + + var rootFolderPath = _appPaths.DefaultUserViewsPath; + + var currentPath = Path.Combine(rootFolderPath, name); + var newPath = Path.Combine(rootFolderPath, newName); + + if (!Directory.Exists(currentPath)) + { + return NotFound("The media collection does not exist."); + } + + if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath)) + { + return Conflict($"The media library already exists at {newPath}."); + } + + _libraryMonitor.Stop(); + + try + { + // Changing capitalization. Handle windows case insensitivity + if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase)) + { + var tempPath = Path.Combine( + rootFolderPath, + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); + Directory.Move(currentPath, tempPath); + currentPath = tempPath; + } + + Directory.Move(currentPath, newPath); + } + finally + { + CollectionFolder.OnCollectionFolderChange(); + + Task.Run(() => + { + // No need to start if scanning the library because it will handle it + if (refreshLibrary) + { + _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + var task = Task.Delay(1000); + // Have to block here to allow exceptions to bubble + Task.WaitAll(task); + + _libraryMonitor.Start(); + } + }); + } + + return NoContent(); + } + + /// + /// Add a media path to a library. + /// + /// The name of the library. + /// The path to add. + /// The path info. + /// Whether to refresh the library. + /// A . + /// Media path added. + /// The name of the library may not be empty. + [HttpPost("Paths")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult AddMediaPath( + [FromQuery] string name, + [FromQuery] string path, + [FromQuery] MediaPathInfo pathInfo, + [FromQuery] bool refreshLibrary) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + _libraryMonitor.Stop(); + + try + { + var mediaPath = pathInfo ?? new MediaPathInfo { Path = path }; + + _libraryManager.AddMediaPath(name, mediaPath); + } + finally + { + Task.Run(() => + { + // No need to start if scanning the library because it will handle it + if (refreshLibrary) + { + _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + var task = Task.Delay(1000); + // Have to block here to allow exceptions to bubble + Task.WaitAll(task); + + _libraryMonitor.Start(); + } + }); + } + + return NoContent(); + } + + /// + /// Updates a media path. + /// + /// The name of the library. + /// The path info. + /// A . + /// Media path updated. + /// The name of the library may not be empty. + [HttpPost("Paths/Update")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult UpdateMediaPath( + [FromQuery] string name, + [FromQuery] MediaPathInfo pathInfo) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + _libraryManager.UpdateMediaPath(name, pathInfo); + return NoContent(); + } + + /// + /// Remove a media path. + /// + /// The name of the library. + /// The path to remove. + /// Whether to refresh the library. + /// A . + /// Media path removed. + /// The name of the library may not be empty. + [HttpDelete("Paths")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult RemoveMediaPath( + [FromQuery] string name, + [FromQuery] string path, + [FromQuery] bool refreshLibrary) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + _libraryMonitor.Stop(); + + try + { + _libraryManager.RemoveMediaPath(name, path); + } + finally + { + Task.Run(() => + { + // No need to start if scanning the library because it will handle it + if (refreshLibrary) + { + _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + var task = Task.Delay(1000); + // Have to block here to allow exceptions to bubble + Task.WaitAll(task); + + _libraryMonitor.Start(); + } + }); + } + + return NoContent(); + } + + /// + /// Update library options. + /// + /// The library name. + /// The library options. + /// Library updated. + /// A . + [HttpPost("LibraryOptions")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult UpdateLibraryOptions( + [FromQuery] string id, + [FromQuery] LibraryOptions libraryOptions) + { + var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id); + + collectionFolder.UpdateLibraryOptions(libraryOptions); + return NoContent(); + } + } +} diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs deleted file mode 100644 index 1e300814f..000000000 --- a/MediaBrowser.Api/Library/LibraryStructureService.cs +++ /dev/null @@ -1,412 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Progress; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.Library -{ - /// - /// Class GetDefaultVirtualFolders - /// - [Route("/Library/VirtualFolders", "GET")] - public class GetVirtualFolders : IReturn> - { - /// - /// Gets or sets the user id. - /// - /// The user id. - public string UserId { get; set; } - } - - [Route("/Library/VirtualFolders", "POST")] - public class AddVirtualFolder : IReturnVoid - { - /// - /// Gets or sets the name. - /// - /// The name. - public string Name { get; set; } - - /// - /// Gets or sets the type of the collection. - /// - /// The type of the collection. - public string CollectionType { get; set; } - - /// - /// Gets or sets a value indicating whether [refresh library]. - /// - /// true if [refresh library]; otherwise, false. - public bool RefreshLibrary { get; set; } - - /// - /// Gets or sets the path. - /// - /// The path. - public string[] Paths { get; set; } - - public LibraryOptions LibraryOptions { get; set; } - } - - [Route("/Library/VirtualFolders", "DELETE")] - public class RemoveVirtualFolder : IReturnVoid - { - /// - /// Gets or sets the name. - /// - /// The name. - public string Name { get; set; } - - /// - /// Gets or sets a value indicating whether [refresh library]. - /// - /// true if [refresh library]; otherwise, false. - public bool RefreshLibrary { get; set; } - } - - [Route("/Library/VirtualFolders/Name", "POST")] - public class RenameVirtualFolder : IReturnVoid - { - /// - /// Gets or sets the name. - /// - /// The name. - public string Name { get; set; } - - /// - /// Gets or sets the name. - /// - /// The name. - public string NewName { get; set; } - - /// - /// Gets or sets a value indicating whether [refresh library]. - /// - /// true if [refresh library]; otherwise, false. - public bool RefreshLibrary { get; set; } - } - - [Route("/Library/VirtualFolders/Paths", "POST")] - public class AddMediaPath : IReturnVoid - { - /// - /// Gets or sets the name. - /// - /// The name. - public string Name { get; set; } - - /// - /// Gets or sets the name. - /// - /// The name. - public string Path { get; set; } - - public MediaPathInfo PathInfo { get; set; } - - /// - /// Gets or sets a value indicating whether [refresh library]. - /// - /// true if [refresh library]; otherwise, false. - public bool RefreshLibrary { get; set; } - } - - [Route("/Library/VirtualFolders/Paths/Update", "POST")] - public class UpdateMediaPath : IReturnVoid - { - /// - /// Gets or sets the name. - /// - /// The name. - public string Name { get; set; } - - public MediaPathInfo PathInfo { get; set; } - } - - [Route("/Library/VirtualFolders/Paths", "DELETE")] - public class RemoveMediaPath : IReturnVoid - { - /// - /// Gets or sets the name. - /// - /// The name. - public string Name { get; set; } - - /// - /// Gets or sets the name. - /// - /// The name. - public string Path { get; set; } - - /// - /// Gets or sets a value indicating whether [refresh library]. - /// - /// true if [refresh library]; otherwise, false. - public bool RefreshLibrary { get; set; } - } - - [Route("/Library/VirtualFolders/LibraryOptions", "POST")] - public class UpdateLibraryOptions : IReturnVoid - { - public string Id { get; set; } - - public LibraryOptions LibraryOptions { get; set; } - } - - /// - /// Class LibraryStructureService - /// - [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)] - public class LibraryStructureService : BaseApiService - { - /// - /// The _app paths - /// - private readonly IServerApplicationPaths _appPaths; - - /// - /// The _library manager - /// - private readonly ILibraryManager _libraryManager; - private readonly ILibraryMonitor _libraryMonitor; - - - /// - /// Initializes a new instance of the class. - /// - public LibraryStructureService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - ILibraryManager libraryManager, - ILibraryMonitor libraryMonitor) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _appPaths = serverConfigurationManager.ApplicationPaths; - _libraryManager = libraryManager; - _libraryMonitor = libraryMonitor; - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetVirtualFolders request) - { - var result = _libraryManager.GetVirtualFolders(true); - - return ToOptimizedResult(result); - } - - public void Post(UpdateLibraryOptions request) - { - var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id); - - collectionFolder.UpdateLibraryOptions(request.LibraryOptions); - } - - /// - /// Posts the specified request. - /// - /// The request. - public Task Post(AddVirtualFolder request) - { - var libraryOptions = request.LibraryOptions ?? new LibraryOptions(); - - if (request.Paths != null && request.Paths.Length > 0) - { - libraryOptions.PathInfos = request.Paths.Select(i => new MediaPathInfo { Path = i }).ToArray(); - } - - return _libraryManager.AddVirtualFolder(request.Name, request.CollectionType, libraryOptions, request.RefreshLibrary); - } - - /// - /// Posts the specified request. - /// - /// The request. - public void Post(RenameVirtualFolder request) - { - if (string.IsNullOrWhiteSpace(request.Name)) - { - throw new ArgumentNullException(nameof(request)); - } - - if (string.IsNullOrWhiteSpace(request.NewName)) - { - throw new ArgumentNullException(nameof(request)); - } - - var rootFolderPath = _appPaths.DefaultUserViewsPath; - - var currentPath = Path.Combine(rootFolderPath, request.Name); - var newPath = Path.Combine(rootFolderPath, request.NewName); - - if (!Directory.Exists(currentPath)) - { - throw new FileNotFoundException("The media collection does not exist"); - } - - if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath)) - { - throw new ArgumentException("Media library already exists at " + newPath + "."); - } - - _libraryMonitor.Stop(); - - try - { - // Changing capitalization. Handle windows case insensitivity - if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase)) - { - var tempPath = Path.Combine(rootFolderPath, Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); - Directory.Move(currentPath, tempPath); - currentPath = tempPath; - } - - Directory.Move(currentPath, newPath); - } - finally - { - CollectionFolder.OnCollectionFolderChange(); - - Task.Run(() => - { - // No need to start if scanning the library because it will handle it - if (request.RefreshLibrary) - { - _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None); - } - else - { - // Need to add a delay here or directory watchers may still pick up the changes - var task = Task.Delay(1000); - // Have to block here to allow exceptions to bubble - Task.WaitAll(task); - - _libraryMonitor.Start(); - } - }); - } - } - - /// - /// Deletes the specified request. - /// - /// The request. - public Task Delete(RemoveVirtualFolder request) - { - return _libraryManager.RemoveVirtualFolder(request.Name, request.RefreshLibrary); - } - - /// - /// Posts the specified request. - /// - /// The request. - public void Post(AddMediaPath request) - { - if (string.IsNullOrWhiteSpace(request.Name)) - { - throw new ArgumentNullException(nameof(request)); - } - - _libraryMonitor.Stop(); - - try - { - var mediaPath = request.PathInfo ?? new MediaPathInfo - { - Path = request.Path - }; - - _libraryManager.AddMediaPath(request.Name, mediaPath); - } - finally - { - Task.Run(() => - { - // No need to start if scanning the library because it will handle it - if (request.RefreshLibrary) - { - _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None); - } - else - { - // Need to add a delay here or directory watchers may still pick up the changes - var task = Task.Delay(1000); - // Have to block here to allow exceptions to bubble - Task.WaitAll(task); - - _libraryMonitor.Start(); - } - }); - } - } - - /// - /// Posts the specified request. - /// - /// The request. - public void Post(UpdateMediaPath request) - { - if (string.IsNullOrWhiteSpace(request.Name)) - { - throw new ArgumentNullException(nameof(request)); - } - - _libraryManager.UpdateMediaPath(request.Name, request.PathInfo); - } - - /// - /// Deletes the specified request. - /// - /// The request. - public void Delete(RemoveMediaPath request) - { - if (string.IsNullOrWhiteSpace(request.Name)) - { - throw new ArgumentNullException(nameof(request)); - } - - _libraryMonitor.Stop(); - - try - { - _libraryManager.RemoveMediaPath(request.Name, request.Path); - } - finally - { - Task.Run(() => - { - // No need to start if scanning the library because it will handle it - if (request.RefreshLibrary) - { - _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None); - } - else - { - // Need to add a delay here or directory watchers may still pick up the changes - var task = Task.Delay(1000); - // Have to block here to allow exceptions to bubble - Task.WaitAll(task); - - _libraryMonitor.Start(); - } - }); - } - } - } -} From edc5611ec7c638164ffe14e4c06055d4fd58b5e8 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 14 Jun 2020 13:50:51 +0200 Subject: [PATCH 2/3] Fix null reference to fix CI --- Jellyfin.Api/Controllers/LibraryStructureController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index f074a61db..ecbfed469 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -1,4 +1,4 @@ -#pragma warning disable CA1801 +#pragma warning disable CA1801 using System; using System.Collections.Generic; @@ -43,7 +43,7 @@ namespace Jellyfin.Api.Controllers ILibraryManager libraryManager, ILibraryMonitor libraryMonitor) { - _appPaths = serverConfigurationManager?.ApplicationPaths; + _appPaths = serverConfigurationManager.ApplicationPaths; _libraryManager = libraryManager; _libraryMonitor = libraryMonitor; } From a952d1567078dae0f5c732063e14a161cd784c0c Mon Sep 17 00:00:00 2001 From: David Date: Tue, 16 Jun 2020 16:08:31 +0200 Subject: [PATCH 3/3] Await Task from _libraryManager --- Jellyfin.Api/Controllers/LibraryStructureController.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index ecbfed469..a989efe7f 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -1,3 +1,4 @@ +#nullable enable #pragma warning disable CA1801 using System; @@ -73,7 +74,7 @@ namespace Jellyfin.Api.Controllers /// A . [HttpPost] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult AddVirtualFolder( + public async Task AddVirtualFolder( [FromQuery] string name, [FromQuery] string collectionType, [FromQuery] bool refreshLibrary, @@ -87,7 +88,7 @@ namespace Jellyfin.Api.Controllers libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo { Path = i }).ToArray(); } - _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary); + await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false); return NoContent(); } @@ -101,11 +102,11 @@ namespace Jellyfin.Api.Controllers /// A . [HttpDelete] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult RemoveVirtualFolder( + public async Task RemoveVirtualFolder( [FromQuery] string name, [FromQuery] bool refreshLibrary) { - _libraryManager.RemoveVirtualFolder(name, refreshLibrary); + await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false); return NoContent(); }