diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
new file mode 100644
index 0000000000..f074a61dbe
--- /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 1e300814f6..0000000000
--- 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();
- }
- });
- }
- }
- }
-}