using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LibraryStructureDto; 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. /// /// Virtual folders retrieved. /// An with the virtual folders. [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetVirtualFolders() { return _libraryManager.GetVirtualFolders(true); } /// /// Adds a virtual folder. /// /// The name of the virtual folder. /// The type of the collection. /// The paths of the virtual folder. /// The library options. /// Whether to refresh the library. /// Folder added. /// A . [HttpPost] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task AddVirtualFolder( [FromQuery] string? name, [FromQuery] CollectionTypeOptions? collectionType, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths, [FromBody] AddVirtualFolderDto? libraryOptionsDto, [FromQuery] bool refreshLibrary = false) { var libraryOptions = libraryOptionsDto?.LibraryOptions ?? new LibraryOptions(); if (paths != null && paths.Length > 0) { libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo(i)).ToArray(); } await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false); return NoContent(); } /// /// Removes a virtual folder. /// /// The name of the folder. /// Whether to refresh the library. /// Folder removed. /// A . [HttpDelete] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task RemoveVirtualFolder( [FromQuery] string? name, [FromQuery] bool refreshLibrary = false) { await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false); 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 = false) { 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(async () => { // No need to start if scanning the library because it will handle it if (refreshLibrary) { await _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None).ConfigureAwait(false); } else { // Need to add a delay here or directory watchers may still pick up the changes // Have to block here to allow exceptions to bubble await Task.Delay(1000).ConfigureAwait(false); _libraryMonitor.Start(); } }); } return NoContent(); } /// /// Add a media path to a library. /// /// The media path dto. /// 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( [FromBody, Required] MediaPathDto mediaPathDto, [FromQuery] bool refreshLibrary = false) { _libraryMonitor.Stop(); try { var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo(mediaPathDto.Path ?? throw new ArgumentException("PathInfo and Path can't both be null.")); _libraryManager.AddMediaPath(mediaPathDto.Name, mediaPath); } finally { Task.Run(async () => { // No need to start if scanning the library because it will handle it if (refreshLibrary) { await _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None).ConfigureAwait(false); } else { // Need to add a delay here or directory watchers may still pick up the changes // Have to block here to allow exceptions to bubble await Task.Delay(1000).ConfigureAwait(false); _libraryMonitor.Start(); } }); } return NoContent(); } /// /// Updates a media path. /// /// The name of the library and path infos. /// A . /// Media path updated. /// The name of the library may not be empty. [HttpPost("Paths/Update")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult UpdateMediaPath([FromBody, Required] UpdateMediaPathRequestDto mediaPathRequestDto) { if (string.IsNullOrWhiteSpace(mediaPathRequestDto.Name)) { throw new ArgumentNullException(nameof(mediaPathRequestDto), "Name must not be null or empty"); } _libraryManager.UpdateMediaPath(mediaPathRequestDto.Name, mediaPathRequestDto.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 = false) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentNullException(nameof(name)); } _libraryMonitor.Stop(); try { _libraryManager.RemoveMediaPath(name, path); } finally { Task.Run(async () => { // No need to start if scanning the library because it will handle it if (refreshLibrary) { await _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None).ConfigureAwait(false); } else { // Need to add a delay here or directory watchers may still pick up the changes // Have to block here to allow exceptions to bubble await Task.Delay(1000).ConfigureAwait(false); _libraryMonitor.Start(); } }); } return NoContent(); } /// /// Update library options. /// /// The library name and options. /// Library updated. /// A . [HttpPost("LibraryOptions")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult UpdateLibraryOptions( [FromBody] UpdateLibraryOptionsDto request) { var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id); collectionFolder.UpdateLibraryOptions(request.LibraryOptions); return NoContent(); } } }