< Summary - Jellyfin

Information
Class: Jellyfin.Api.Controllers.LibraryStructureController
Assembly: Jellyfin.Api
File(s): /srv/git/jellyfin/Jellyfin.Api/Controllers/LibraryStructureController.cs
Line coverage
54%
Covered lines: 62
Uncovered lines: 51
Coverable lines: 113
Total lines: 345
Line coverage: 54.8%
Branch coverage
50%
Covered branches: 10
Total branches: 20
Branch coverage: 50%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetVirtualFolders()100%11100%
RenameVirtualFolder(...)41.66%93.451217.3%
AddMediaPath(...)75%4.01490.47%
UpdateMediaPath(...)0%620%
RemoveMediaPath(...)100%1190.9%
UpdateLibraryOptions(...)100%22100%

File(s)

/srv/git/jellyfin/Jellyfin.Api/Controllers/LibraryStructureController.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.ComponentModel.DataAnnotations;
 4using System.Globalization;
 5using System.IO;
 6using System.Linq;
 7using System.Threading;
 8using System.Threading.Tasks;
 9using Jellyfin.Api.Extensions;
 10using Jellyfin.Api.Helpers;
 11using Jellyfin.Api.ModelBinders;
 12using Jellyfin.Api.Models.LibraryStructureDto;
 13using MediaBrowser.Common.Api;
 14using MediaBrowser.Controller;
 15using MediaBrowser.Controller.Configuration;
 16using MediaBrowser.Controller.Entities;
 17using MediaBrowser.Controller.Library;
 18using MediaBrowser.Model.Configuration;
 19using MediaBrowser.Model.Entities;
 20using Microsoft.AspNetCore.Authorization;
 21using Microsoft.AspNetCore.Http;
 22using Microsoft.AspNetCore.Mvc;
 23
 24namespace Jellyfin.Api.Controllers;
 25
 26/// <summary>
 27/// The library structure controller.
 28/// </summary>
 29[Route("Library/VirtualFolders")]
 30[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
 31public class LibraryStructureController : BaseJellyfinApiController
 32{
 33    private readonly IServerApplicationPaths _appPaths;
 34    private readonly ILibraryManager _libraryManager;
 35    private readonly ILibraryMonitor _libraryMonitor;
 36
 37    /// <summary>
 38    /// Initializes a new instance of the <see cref="LibraryStructureController"/> class.
 39    /// </summary>
 40    /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param
 41    /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
 42    /// <param name="libraryMonitor">Instance of <see cref="ILibraryMonitor"/> interface.</param>
 1343    public LibraryStructureController(
 1344        IServerConfigurationManager serverConfigurationManager,
 1345        ILibraryManager libraryManager,
 1346        ILibraryMonitor libraryMonitor)
 47    {
 1348        _appPaths = serverConfigurationManager.ApplicationPaths;
 1349        _libraryManager = libraryManager;
 1350        _libraryMonitor = libraryMonitor;
 1351    }
 52
 53    /// <summary>
 54    /// Gets all virtual folders.
 55    /// </summary>
 56    /// <response code="200">Virtual folders retrieved.</response>
 57    /// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns>
 58    [HttpGet]
 59    [ProducesResponseType(StatusCodes.Status200OK)]
 60    public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders()
 61    {
 162        return _libraryManager.GetVirtualFolders(true);
 63    }
 64
 65    /// <summary>
 66    /// Adds a virtual folder.
 67    /// </summary>
 68    /// <param name="name">The name of the virtual folder.</param>
 69    /// <param name="collectionType">The type of the collection.</param>
 70    /// <param name="paths">The paths of the virtual folder.</param>
 71    /// <param name="libraryOptionsDto">The library options.</param>
 72    /// <param name="refreshLibrary">Whether to refresh the library.</param>
 73    /// <response code="204">Folder added.</response>
 74    /// <returns>A <see cref="NoContentResult"/>.</returns>
 75    [HttpPost]
 76    [ProducesResponseType(StatusCodes.Status204NoContent)]
 77    public async Task<ActionResult> AddVirtualFolder(
 78        [FromQuery] string name,
 79        [FromQuery] CollectionTypeOptions? collectionType,
 80        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
 81        [FromBody] AddVirtualFolderDto? libraryOptionsDto,
 82        [FromQuery] bool refreshLibrary = false)
 83    {
 84        var libraryOptions = libraryOptionsDto?.LibraryOptions ?? new LibraryOptions();
 85
 86        if (paths is not null && paths.Length > 0)
 87        {
 88            libraryOptions.PathInfos = Array.ConvertAll(paths, i => new MediaPathInfo(i));
 89        }
 90
 91        await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(fals
 92
 93        return NoContent();
 94    }
 95
 96    /// <summary>
 97    /// Removes a virtual folder.
 98    /// </summary>
 99    /// <param name="name">The name of the folder.</param>
 100    /// <param name="refreshLibrary">Whether to refresh the library.</param>
 101    /// <response code="204">Folder removed.</response>
 102    /// <returns>A <see cref="NoContentResult"/>.</returns>
 103    [HttpDelete]
 104    [ProducesResponseType(StatusCodes.Status204NoContent)]
 105    public async Task<ActionResult> RemoveVirtualFolder(
 106        [FromQuery] string name,
 107        [FromQuery] bool refreshLibrary = false)
 108    {
 109        await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
 110        return NoContent();
 111    }
 112
 113    /// <summary>
 114    /// Renames a virtual folder.
 115    /// </summary>
 116    /// <param name="name">The name of the virtual folder.</param>
 117    /// <param name="newName">The new name.</param>
 118    /// <param name="refreshLibrary">Whether to refresh the library.</param>
 119    /// <response code="204">Folder renamed.</response>
 120    /// <response code="404">Library doesn't exist.</response>
 121    /// <response code="409">Library already exists.</response>
 122    /// <returns>A <see cref="NoContentResult"/> on success, a <see cref="NotFoundResult"/> if the library doesn't exist
 123    /// <exception cref="ArgumentNullException">The new name may not be null.</exception>
 124    [HttpPost("Name")]
 125    [ProducesResponseType(StatusCodes.Status204NoContent)]
 126    [ProducesResponseType(StatusCodes.Status404NotFound)]
 127    [ProducesResponseType(StatusCodes.Status409Conflict)]
 128    public ActionResult RenameVirtualFolder(
 129        [FromQuery] string? name,
 130        [FromQuery] string? newName,
 131        [FromQuery] bool refreshLibrary = false)
 132    {
 3133        if (string.IsNullOrWhiteSpace(name))
 134        {
 1135            throw new ArgumentNullException(nameof(name));
 136        }
 137
 2138        if (string.IsNullOrWhiteSpace(newName))
 139        {
 1140            throw new ArgumentNullException(nameof(newName));
 141        }
 142
 1143        var rootFolderPath = _appPaths.DefaultUserViewsPath;
 144
 1145        var currentPath = Path.Combine(rootFolderPath, name);
 1146        var newPath = Path.Combine(rootFolderPath, newName);
 147
 1148        if (!Directory.Exists(currentPath))
 149        {
 1150            return NotFound("The media collection does not exist.");
 151        }
 152
 0153        if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath))
 154        {
 0155            return Conflict($"The media library already exists at {newPath}.");
 156        }
 157
 0158        _libraryMonitor.Stop();
 159
 160        try
 161        {
 162            // Changing capitalization. Handle windows case insensitivity
 0163            if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase))
 164            {
 0165                var tempPath = Path.Combine(
 0166                    rootFolderPath,
 0167                    Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture));
 0168                Directory.Move(currentPath, tempPath);
 0169                currentPath = tempPath;
 170            }
 171
 0172            Directory.Move(currentPath, newPath);
 0173        }
 174        finally
 175        {
 0176            CollectionFolder.OnCollectionFolderChange();
 177
 0178            Task.Run(async () =>
 0179            {
 0180                // No need to start if scanning the library because it will handle it
 0181                if (refreshLibrary)
 0182                {
 0183                    await _libraryManager.ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
 0184                    var newLib = _libraryManager.GetUserRootFolder().Children.FirstOrDefault(f => f.Path.Equals(newPath,
 0185                    if (newLib is CollectionFolder folder)
 0186                    {
 0187                        foreach (var child in folder.GetPhysicalFolders())
 0188                        {
 0189                            await child.RefreshMetadata(CancellationToken.None).ConfigureAwait(false);
 0190                            await child.ValidateChildren(new Progress<double>(), CancellationToken.None).ConfigureAwait(
 0191                        }
 0192                    }
 0193                    else
 0194                    {
 0195                        // We don't know if this one can be validated individually, trigger a new validation
 0196                        await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).Confi
 0197                    }
 0198                }
 0199                else
 0200                {
 0201                    // Need to add a delay here or directory watchers may still pick up the changes
 0202                    // Have to block here to allow exceptions to bubble
 0203                    await Task.Delay(1000).ConfigureAwait(false);
 0204                    _libraryMonitor.Start();
 0205                }
 0206            });
 0207        }
 208
 0209        return NoContent();
 210    }
 211
 212    /// <summary>
 213    /// Add a media path to a library.
 214    /// </summary>
 215    /// <param name="mediaPathDto">The media path dto.</param>
 216    /// <param name="refreshLibrary">Whether to refresh the library.</param>
 217    /// <returns>A <see cref="NoContentResult"/>.</returns>
 218    /// <response code="204">Media path added.</response>
 219    /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
 220    [HttpPost("Paths")]
 221    [ProducesResponseType(StatusCodes.Status204NoContent)]
 222    public ActionResult AddMediaPath(
 223        [FromBody, Required] MediaPathDto mediaPathDto,
 224        [FromQuery] bool refreshLibrary = false)
 225    {
 1226        _libraryMonitor.Stop();
 227
 228        try
 229        {
 1230            var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo(mediaPathDto.Path ?? throw new ArgumentException(
 231
 1232            _libraryManager.AddMediaPath(mediaPathDto.Name, mediaPath);
 0233        }
 234        finally
 235        {
 1236            Task.Run(async () =>
 1237            {
 1238                // No need to start if scanning the library because it will handle it
 1239                if (refreshLibrary)
 1240                {
 1241                    await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).Configure
 1242                }
 1243                else
 1244                {
 1245                    // Need to add a delay here or directory watchers may still pick up the changes
 1246                    // Have to block here to allow exceptions to bubble
 1247                    await Task.Delay(1000).ConfigureAwait(false);
 1248                    _libraryMonitor.Start();
 1249                }
 1250            });
 1251        }
 252
 0253        return NoContent();
 254    }
 255
 256    /// <summary>
 257    /// Updates a media path.
 258    /// </summary>
 259    /// <param name="mediaPathRequestDto">The name of the library and path infos.</param>
 260    /// <returns>A <see cref="NoContentResult"/>.</returns>
 261    /// <response code="204">Media path updated.</response>
 262    /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
 263    [HttpPost("Paths/Update")]
 264    [ProducesResponseType(StatusCodes.Status204NoContent)]
 265    public ActionResult UpdateMediaPath([FromBody, Required] UpdateMediaPathRequestDto mediaPathRequestDto)
 266    {
 0267        if (string.IsNullOrWhiteSpace(mediaPathRequestDto.Name))
 268        {
 0269            throw new ArgumentNullException(nameof(mediaPathRequestDto), "Name must not be null or empty");
 270        }
 271
 0272        _libraryManager.UpdateMediaPath(mediaPathRequestDto.Name, mediaPathRequestDto.PathInfo);
 0273        return NoContent();
 274    }
 275
 276    /// <summary>
 277    /// Remove a media path.
 278    /// </summary>
 279    /// <param name="name">The name of the library.</param>
 280    /// <param name="path">The path to remove.</param>
 281    /// <param name="refreshLibrary">Whether to refresh the library.</param>
 282    /// <returns>A <see cref="NoContentResult"/>.</returns>
 283    /// <response code="204">Media path removed.</response>
 284    /// <exception cref="ArgumentException">The name of the library and path may not be empty.</exception>
 285    [HttpDelete("Paths")]
 286    [ProducesResponseType(StatusCodes.Status204NoContent)]
 287    public ActionResult RemoveMediaPath(
 288        [FromQuery] string name,
 289        [FromQuery] string path,
 290        [FromQuery] bool refreshLibrary = false)
 291    {
 1292        ArgumentException.ThrowIfNullOrWhiteSpace(name);
 1293        ArgumentException.ThrowIfNullOrWhiteSpace(path);
 294
 1295        _libraryMonitor.Stop();
 296
 297        try
 298        {
 1299            _libraryManager.RemoveMediaPath(name, path);
 0300        }
 301        finally
 302        {
 1303            Task.Run(async () =>
 1304            {
 1305                // No need to start if scanning the library because it will handle it
 1306                if (refreshLibrary)
 1307                {
 1308                    await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).Configure
 1309                }
 1310                else
 1311                {
 1312                    // Need to add a delay here or directory watchers may still pick up the changes
 1313                    // Have to block here to allow exceptions to bubble
 1314                    await Task.Delay(1000).ConfigureAwait(false);
 1315                    _libraryMonitor.Start();
 1316                }
 1317            });
 1318        }
 319
 0320        return NoContent();
 321    }
 322
 323    /// <summary>
 324    /// Update library options.
 325    /// </summary>
 326    /// <param name="request">The library name and options.</param>
 327    /// <response code="204">Library updated.</response>
 328    /// <response code="404">Item not found.</response>
 329    /// <returns>A <see cref="NoContentResult"/>.</returns>
 330    [HttpPost("LibraryOptions")]
 331    [ProducesResponseType(StatusCodes.Status204NoContent)]
 332    [ProducesResponseType(StatusCodes.Status404NotFound)]
 333    public ActionResult UpdateLibraryOptions(
 334        [FromBody] UpdateLibraryOptionsDto request)
 335    {
 2336        var item = _libraryManager.GetItemById<CollectionFolder>(request.Id);
 2337        if (item is null)
 338        {
 1339            return NotFound();
 340        }
 341
 1342        item.UpdateLibraryOptions(request.LibraryOptions);
 1343        return NoContent();
 344    }
 345}