2020-06-12 15:39:06 +02:00
using System ;
using System.Collections.Generic ;
2020-08-06 16:17:45 +02:00
using System.ComponentModel.DataAnnotations ;
2020-06-12 15:39:06 +02:00
using System.Globalization ;
using System.IO ;
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
2024-04-14 16:18:36 +02:00
using Jellyfin.Api.Extensions ;
using Jellyfin.Api.Helpers ;
2020-10-14 21:03:36 +02:00
using Jellyfin.Api.ModelBinders ;
2020-07-25 01:22:32 +02:00
using Jellyfin.Api.Models.LibraryStructureDto ;
2023-11-10 17:17:22 +01:00
using MediaBrowser.Common.Api ;
2020-06-12 15:39:06 +02:00
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 ;
2023-01-31 12:18:10 +01:00
namespace Jellyfin.Api.Controllers ;
/// <summary>
/// The library structure controller.
/// </summary>
[Route("Library/VirtualFolders")]
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
public class LibraryStructureController : BaseJellyfinApiController
2020-06-12 15:39:06 +02:00
{
2023-01-31 12:18:10 +01:00
private readonly IServerApplicationPaths _appPaths ;
private readonly ILibraryManager _libraryManager ;
private readonly ILibraryMonitor _libraryMonitor ;
2020-06-12 15:39:06 +02:00
/// <summary>
2023-01-31 12:18:10 +01:00
/// Initializes a new instance of the <see cref="LibraryStructureController"/> class.
2020-06-12 15:39:06 +02:00
/// </summary>
2023-01-31 12:18:10 +01:00
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
/// <param name="libraryMonitor">Instance of <see cref="ILibraryMonitor"/> interface.</param>
public LibraryStructureController (
IServerConfigurationManager serverConfigurationManager ,
ILibraryManager libraryManager ,
ILibraryMonitor libraryMonitor )
2020-06-12 15:39:06 +02:00
{
2023-01-31 12:18:10 +01:00
_appPaths = serverConfigurationManager . ApplicationPaths ;
_libraryManager = libraryManager ;
_libraryMonitor = libraryMonitor ;
}
/// <summary>
/// Gets all virtual folders.
/// </summary>
/// <response code="200">Virtual folders retrieved.</response>
/// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult < IEnumerable < VirtualFolderInfo > > GetVirtualFolders ( )
{
return _libraryManager . GetVirtualFolders ( true ) ;
}
/// <summary>
/// Adds a virtual folder.
/// </summary>
/// <param name="name">The name of the virtual folder.</param>
/// <param name="collectionType">The type of the collection.</param>
/// <param name="paths">The paths of the virtual folder.</param>
/// <param name="libraryOptionsDto">The library options.</param>
/// <param name="refreshLibrary">Whether to refresh the library.</param>
/// <response code="204">Folder added.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task < ActionResult > AddVirtualFolder (
2024-04-17 18:44:50 +02:00
[FromQuery] string name ,
2023-01-31 12:18:10 +01:00
[FromQuery] CollectionTypeOptions ? collectionType ,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string [ ] paths ,
[FromBody] AddVirtualFolderDto ? libraryOptionsDto ,
[FromQuery] bool refreshLibrary = false )
{
var libraryOptions = libraryOptionsDto ? . LibraryOptions ? ? new LibraryOptions ( ) ;
2020-06-12 15:39:06 +02:00
2023-01-31 12:18:10 +01:00
if ( paths is not null & & paths . Length > 0 )
2020-06-12 15:39:06 +02:00
{
2023-01-31 12:18:10 +01:00
libraryOptions . PathInfos = paths . Select ( i = > new MediaPathInfo ( i ) ) . ToArray ( ) ;
2020-06-12 15:39:06 +02:00
}
2023-01-31 12:18:10 +01:00
await _libraryManager . AddVirtualFolder ( name , collectionType , libraryOptions , refreshLibrary ) . ConfigureAwait ( false ) ;
2020-06-12 15:39:06 +02:00
2023-01-31 12:18:10 +01:00
return NoContent ( ) ;
}
2020-06-12 15:39:06 +02:00
2023-01-31 12:18:10 +01:00
/// <summary>
/// Removes a virtual folder.
/// </summary>
/// <param name="name">The name of the folder.</param>
/// <param name="refreshLibrary">Whether to refresh the library.</param>
/// <response code="204">Folder removed.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task < ActionResult > RemoveVirtualFolder (
2024-04-17 18:44:50 +02:00
[FromQuery] string name ,
2023-01-31 12:18:10 +01:00
[FromQuery] bool refreshLibrary = false )
{
await _libraryManager . RemoveVirtualFolder ( name , refreshLibrary ) . ConfigureAwait ( false ) ;
return NoContent ( ) ;
}
2020-06-12 15:39:06 +02:00
2023-01-31 12:18:10 +01:00
/// <summary>
/// Renames a virtual folder.
/// </summary>
/// <param name="name">The name of the virtual folder.</param>
/// <param name="newName">The new name.</param>
/// <param name="refreshLibrary">Whether to refresh the library.</param>
/// <response code="204">Folder renamed.</response>
/// <response code="404">Library doesn't exist.</response>
/// <response code="409">Library already exists.</response>
/// <returns>A <see cref="NoContentResult"/> on success, a <see cref="NotFoundResult"/> if the library doesn't exist, a <see cref="ConflictResult"/> if the new name is already taken.</returns>
/// <exception cref="ArgumentNullException">The new name may not be null.</exception>
[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 ) ) ;
2020-06-12 15:39:06 +02:00
}
2023-01-31 12:18:10 +01:00
if ( string . IsNullOrWhiteSpace ( newName ) )
2020-06-12 15:39:06 +02:00
{
2023-01-31 12:18:10 +01:00
throw new ArgumentNullException ( nameof ( newName ) ) ;
2020-06-12 15:39:06 +02:00
}
2023-01-31 12:18:10 +01:00
var rootFolderPath = _appPaths . DefaultUserViewsPath ;
2020-06-12 15:39:06 +02:00
2023-01-31 12:18:10 +01:00
var currentPath = Path . Combine ( rootFolderPath , name ) ;
var newPath = Path . Combine ( rootFolderPath , newName ) ;
2020-06-12 15:39:06 +02:00
2023-01-31 12:18:10 +01:00
if ( ! Directory . Exists ( currentPath ) )
{
return NotFound ( "The media collection does not exist." ) ;
}
2020-06-12 15:39:06 +02:00
2023-01-31 12:18:10 +01:00
if ( ! string . Equals ( currentPath , newPath , StringComparison . OrdinalIgnoreCase ) & & Directory . Exists ( newPath ) )
{
return Conflict ( $"The media library already exists at {newPath}." ) ;
}
2020-06-12 15:39:06 +02:00
2023-01-31 12:18:10 +01:00
_libraryMonitor . Stop ( ) ;
2020-06-12 15:39:06 +02:00
2023-01-31 12:18:10 +01:00
try
{
// Changing capitalization. Handle windows case insensitivity
if ( string . Equals ( currentPath , newPath , StringComparison . OrdinalIgnoreCase ) )
2020-06-12 15:39:06 +02:00
{
2023-01-31 12:18:10 +01:00
var tempPath = Path . Combine (
rootFolderPath ,
Guid . NewGuid ( ) . ToString ( "N" , CultureInfo . InvariantCulture ) ) ;
Directory . Move ( currentPath , tempPath ) ;
currentPath = tempPath ;
2020-06-12 15:39:06 +02:00
}
2023-01-31 12:18:10 +01:00
Directory . Move ( currentPath , newPath ) ;
}
finally
{
CollectionFolder . OnCollectionFolderChange ( ) ;
2020-06-12 15:39:06 +02:00
2023-01-31 12:18:10 +01:00
Task . Run ( async ( ) = >
2020-06-12 15:39:06 +02:00
{
2023-01-31 12:18:10 +01:00
// No need to start if scanning the library because it will handle it
if ( refreshLibrary )
2020-06-12 15:39:06 +02:00
{
2024-02-06 15:50:46 +01:00
await _libraryManager . ValidateMediaLibrary ( new Progress < double > ( ) , CancellationToken . None ) . ConfigureAwait ( false ) ;
2020-06-12 15:39:06 +02:00
}
2023-01-31 12:18:10 +01:00
else
2020-06-12 15:39:06 +02:00
{
2023-01-31 12:18:10 +01:00
// 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 ( ) ;
}
} ) ;
2020-06-12 15:39:06 +02:00
}
2023-01-31 12:18:10 +01:00
return NoContent ( ) ;
}
2020-06-12 15:39:06 +02:00
2023-01-31 12:18:10 +01:00
/// <summary>
/// Add a media path to a library.
/// </summary>
/// <param name="mediaPathDto">The media path dto.</param>
/// <param name="refreshLibrary">Whether to refresh the library.</param>
/// <returns>A <see cref="NoContentResult"/>.</returns>
/// <response code="204">Media path added.</response>
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
[HttpPost("Paths")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult AddMediaPath (
[FromBody, Required] MediaPathDto mediaPathDto ,
[FromQuery] bool refreshLibrary = false )
{
_libraryMonitor . Stop ( ) ;
2020-06-12 15:39:06 +02:00
2023-01-31 12:18:10 +01:00
try
{
var mediaPath = mediaPathDto . PathInfo ? ? new MediaPathInfo ( mediaPathDto . Path ? ? throw new ArgumentException ( "PathInfo and Path can't both be null." ) ) ;
2020-06-12 15:39:06 +02:00
2023-01-31 12:18:10 +01:00
_libraryManager . AddMediaPath ( mediaPathDto . Name , mediaPath ) ;
2020-06-12 15:39:06 +02:00
}
2023-01-31 12:18:10 +01:00
finally
2020-06-12 15:39:06 +02:00
{
2023-01-31 12:18:10 +01:00
Task . Run ( async ( ) = >
2020-06-12 15:39:06 +02:00
{
2023-01-31 12:18:10 +01:00
// No need to start if scanning the library because it will handle it
if ( refreshLibrary )
{
2024-02-06 15:50:46 +01:00
await _libraryManager . ValidateMediaLibrary ( new Progress < double > ( ) , CancellationToken . None ) . ConfigureAwait ( false ) ;
2023-01-31 12:18:10 +01:00
}
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 ( ) ;
}
2020-06-12 15:39:06 +02:00
2023-01-31 12:18:10 +01:00
/// <summary>
/// Updates a media path.
/// </summary>
/// <param name="mediaPathRequestDto">The name of the library and path infos.</param>
/// <returns>A <see cref="NoContentResult"/>.</returns>
/// <response code="204">Media path updated.</response>
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
[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" ) ;
2020-06-12 15:39:06 +02:00
}
2023-01-31 12:18:10 +01:00
_libraryManager . UpdateMediaPath ( mediaPathRequestDto . Name , mediaPathRequestDto . PathInfo ) ;
return NoContent ( ) ;
}
/// <summary>
/// Remove a media path.
/// </summary>
/// <param name="name">The name of the library.</param>
/// <param name="path">The path to remove.</param>
/// <param name="refreshLibrary">Whether to refresh the library.</param>
/// <returns>A <see cref="NoContentResult"/>.</returns>
/// <response code="204">Media path removed.</response>
2024-04-17 18:44:50 +02:00
/// <exception cref="ArgumentException">The name of the library and path may not be empty.</exception>
2023-01-31 12:18:10 +01:00
[HttpDelete("Paths")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RemoveMediaPath (
2024-04-17 18:44:50 +02:00
[FromQuery] string name ,
[FromQuery] string path ,
2023-01-31 12:18:10 +01:00
[FromQuery] bool refreshLibrary = false )
{
2024-04-17 18:44:50 +02:00
ArgumentException . ThrowIfNullOrWhiteSpace ( name ) ;
ArgumentException . ThrowIfNullOrWhiteSpace ( path ) ;
2020-06-12 15:39:06 +02:00
2023-01-31 12:18:10 +01:00
_libraryMonitor . Stop ( ) ;
2020-06-12 15:39:06 +02:00
2023-01-31 12:18:10 +01:00
try
{
_libraryManager . RemoveMediaPath ( name , path ) ;
}
finally
{
Task . Run ( async ( ) = >
2020-06-12 15:39:06 +02:00
{
2023-01-31 12:18:10 +01:00
// No need to start if scanning the library because it will handle it
if ( refreshLibrary )
2020-06-12 15:39:06 +02:00
{
2024-02-06 15:50:46 +01:00
await _libraryManager . ValidateMediaLibrary ( new Progress < double > ( ) , CancellationToken . None ) . ConfigureAwait ( false ) ;
2023-01-31 12:18:10 +01:00
}
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 ( ) ;
}
} ) ;
2020-06-12 15:39:06 +02:00
}
2023-01-31 12:18:10 +01:00
return NoContent ( ) ;
}
2020-06-12 15:39:06 +02:00
2023-01-31 12:18:10 +01:00
/// <summary>
/// Update library options.
/// </summary>
/// <param name="request">The library name and options.</param>
/// <response code="204">Library updated.</response>
2024-04-14 16:18:36 +02:00
/// <response code="404">Item not found.</response>
2023-01-31 12:18:10 +01:00
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("LibraryOptions")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
2024-04-14 16:18:36 +02:00
[ProducesResponseType(StatusCodes.Status404NotFound)]
2023-01-31 12:18:10 +01:00
public ActionResult UpdateLibraryOptions (
[FromBody] UpdateLibraryOptionsDto request )
{
2024-04-14 16:18:36 +02:00
var item = _libraryManager . GetItemById < CollectionFolder > ( request . Id , User . GetUserId ( ) ) ;
if ( item is null )
{
return NotFound ( ) ;
}
2023-01-31 12:18:10 +01:00
2024-04-14 16:18:36 +02:00
item . UpdateLibraryOptions ( request . LibraryOptions ) ;
2023-01-31 12:18:10 +01:00
return NoContent ( ) ;
2020-06-12 15:39:06 +02:00
}
}