aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs33
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs188
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs18
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs125
4 files changed, 319 insertions, 45 deletions
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 2d62fdab48..b4f6f57bf6 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -3052,14 +3052,7 @@ namespace Emby.Server.Implementations.Library
CollectionType? viewType,
string sortName)
{
- var parentIdString = parentId.IsEmpty()
- ? null
- : parentId.ToString("N", CultureInfo.InvariantCulture);
-
- // The name is either localized (grouped views) or the library folder's own name.
- var idValues = "38_namedview_" + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
-
- var id = GetNewItemId(idValues, typeof(UserView));
+ var id = GetNamedViewId(user, parentId, viewType);
var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture));
@@ -3117,6 +3110,18 @@ namespace Emby.Server.Implementations.Library
return item;
}
+ public Guid GetNamedViewId(User user, Guid parentId, CollectionType? viewType)
+ {
+ var parentIdString = parentId.IsEmpty()
+ ? null
+ : parentId.ToString("N", CultureInfo.InvariantCulture);
+
+ // The name is either localized (grouped views) or the library folder's own name.
+ var idValues = "38_namedview_" + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
+
+ return GetNewItemId(idValues, typeof(UserView));
+ }
+
public UserView GetShadowView(
BaseItem parent,
CollectionType? viewType,
@@ -3126,10 +3131,7 @@ namespace Emby.Server.Implementations.Library
var name = parent.Name;
var parentId = parent.Id;
-
- var idValues = "38_namedview_" + name + parentId + (viewType?.ToString() ?? string.Empty);
-
- var id = GetNewItemId(idValues, typeof(UserView));
+ var id = GetShadowViewId(name, parentId, viewType);
var path = parent.Path;
@@ -3181,6 +3183,13 @@ namespace Emby.Server.Implementations.Library
return item;
}
+ public Guid GetShadowViewId(string name, Guid parentId, CollectionType? viewType)
+ {
+ var idValues = "38_namedview_" + name + parentId + (viewType?.ToString() ?? string.Empty);
+
+ return GetNewItemId(idValues, typeof(UserView));
+ }
+
public UserView GetNamedView(
string name,
Guid parentId,
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index 7f63e410a5..9e6bfde085 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -10,6 +10,10 @@ using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LibraryStructureDto;
+using Jellyfin.Data;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
@@ -36,6 +40,7 @@ public class LibraryStructureController : BaseJellyfinApiController
private readonly ILibraryManager _libraryManager;
private readonly ILibraryMonitor _libraryMonitor;
private readonly IDirectoryService _directoryService;
+ private readonly IUserManager _userManager;
/// <summary>
/// Initializes a new instance of the <see cref="LibraryStructureController"/> class.
@@ -44,16 +49,19 @@ public class LibraryStructureController : BaseJellyfinApiController
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
/// <param name="libraryMonitor">Instance of <see cref="ILibraryMonitor"/> interface.</param>
/// <param name="directoryService">Instance of <see cref="IDirectoryService"/> interface.</param>
+ /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
public LibraryStructureController(
IServerConfigurationManager serverConfigurationManager,
ILibraryManager libraryManager,
ILibraryMonitor libraryMonitor,
- IDirectoryService directoryService)
+ IDirectoryService directoryService,
+ IUserManager userManager)
{
_appPaths = serverConfigurationManager.ApplicationPaths;
_libraryManager = libraryManager;
_libraryMonitor = libraryMonitor;
_directoryService = directoryService;
+ _userManager = userManager;
}
/// <summary>
@@ -138,7 +146,7 @@ public class LibraryStructureController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
- public ActionResult RenameVirtualFolder(
+ public async Task<ActionResult> RenameVirtualFolder(
[FromQuery] string? name,
[FromQuery] string? newName,
[FromQuery] bool refreshLibrary = false)
@@ -173,6 +181,12 @@ public class LibraryStructureController : BaseJellyfinApiController
return Conflict($"The media library already exists at {newPath}.");
}
+ var oldLibrary = FindLibrary(currentPath);
+ // Use the stored ID when possible. The case sensitivity setting may have changed since the library was created.
+ var oldLibraryId = oldLibrary?.Id ?? _libraryManager.GetNewItemId(currentPath, typeof(CollectionFolder));
+ var oldLibraryName = oldLibrary?.Name ?? Path.GetFileName(currentPath);
+ var newLibraryId = _libraryManager.GetNewItemId(newPath, typeof(CollectionFolder));
+
_libraryMonitor.Stop();
try
@@ -188,47 +202,155 @@ public class LibraryStructureController : BaseJellyfinApiController
}
_directoryService.Move(currentPath, newPath);
+
+ await ReplaceLibraryIdInUserPreferencesAsync(
+ oldLibraryId,
+ newLibraryId,
+ oldLibraryName,
+ Path.GetFileName(newPath),
+ oldLibrary?.CollectionType).ConfigureAwait(false);
}
finally
{
CollectionFolder.OnCollectionFolderChange();
- Task.Run(async () =>
+ _ = Task.Run(() => RefreshAfterRenameAsync(newPath, refreshLibrary));
+ }
+
+ return NoContent();
+ }
+
+ private async Task RefreshAfterRenameAsync(string newPath, bool refreshLibrary)
+ {
+ // No need to start if scanning the library because it will handle it
+ if (refreshLibrary)
+ {
+ await _libraryManager.ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
+ var newLib = _libraryManager.GetUserRootFolder().Children.FirstOrDefault(f => f.Path.Equals(newPath, StringComparison.OrdinalIgnoreCase));
+ if (newLib is CollectionFolder folder)
{
- // No need to start if scanning the library because it will handle it
- if (refreshLibrary)
+ _libraryManager.ClearIgnoreRuleCache();
+ foreach (var child in folder.GetPhysicalFolders())
{
- await _libraryManager.ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
- var newLib = _libraryManager.GetUserRootFolder().Children.FirstOrDefault(f => f.Path.Equals(newPath, StringComparison.OrdinalIgnoreCase));
- if (newLib is CollectionFolder folder)
- {
- _libraryManager.ClearIgnoreRuleCache();
- foreach (var child in folder.GetPhysicalFolders())
- {
- await child.RefreshMetadata(CancellationToken.None).ConfigureAwait(false);
- await child.ValidateChildren(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
- }
- }
- else
- {
- _libraryManager.ClearIgnoreRuleCache();
- // We don't know if this one can be validated individually, trigger a new validation
- _libraryManager.QueueLibraryScan();
- }
-
- _libraryManager.ClearIgnoreRuleCache();
- }
- 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();
+ await child.RefreshMetadata(CancellationToken.None).ConfigureAwait(false);
+ await child.ValidateChildren(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
}
- });
+ }
+ else
+ {
+ _libraryManager.ClearIgnoreRuleCache();
+ // We don't know if this one can be validated individually, trigger a new validation
+ _libraryManager.QueueLibraryScan();
+ }
+
+ _libraryManager.ClearIgnoreRuleCache();
}
+ 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();
+ private CollectionFolder? FindLibrary(string path)
+ => _libraryManager.GetUserRootFolder().Children
+ .OfType<CollectionFolder>()
+ .FirstOrDefault(f => string.Equals(f.Path, path, StringComparison.OrdinalIgnoreCase));
+
+ private async Task ReplaceLibraryIdInUserPreferencesAsync(
+ Guid oldId,
+ Guid newId,
+ string oldName,
+ string newName,
+ CollectionType? viewType)
+ {
+ var shadowView = (
+ Old: _libraryManager.GetShadowViewId(oldName, oldId, viewType),
+ New: _libraryManager.GetShadowViewId(newName, newId, viewType));
+
+ if (oldId.Equals(newId) && shadowView.Old.Equals(shadowView.New))
+ {
+ return;
+ }
+
+ PreferenceKind[] libraryPreferences =
+ [
+ PreferenceKind.EnabledFolders,
+ PreferenceKind.BlockedMediaFolders,
+ PreferenceKind.EnableContentDeletionFromFolders,
+ PreferenceKind.LatestItemExcludes,
+ PreferenceKind.MyMediaExcludes,
+ PreferenceKind.GroupedFolders,
+ PreferenceKind.OrderedViews
+ ];
+
+ // These lists hold what the "My Media" row shows, which includes the views built from a library.
+ PreferenceKind[] viewPreferences = [PreferenceKind.MyMediaExcludes, PreferenceKind.OrderedViews];
+
+ foreach (var user in _userManager.GetUsers())
+ {
+ var libraryMap = new Dictionary<Guid, Guid> { [oldId] = newId };
+ var viewMap = new Dictionary<Guid, Guid>(libraryMap)
+ {
+ [shadowView.Old] = shadowView.New,
+ [_libraryManager.GetNamedViewId(user, oldId, viewType)] = _libraryManager.GetNamedViewId(user, newId, viewType)
+ };
+
+ var changed = false;
+
+ foreach (var kind in libraryPreferences)
+ {
+ changed |= ReplaceIds(user, kind, viewPreferences.Contains(kind) ? viewMap : libraryMap);
+ }
+
+ if (changed)
+ {
+ await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+ }
+ }
+ }
+
+ private static bool ReplaceIds(User user, PreferenceKind kind, Dictionary<Guid, Guid> map)
+ {
+ var values = user.GetPreference(kind);
+ var rewritten = new List<string>(values.Length);
+ var seen = new HashSet<Guid>();
+ var touched = false;
+
+ foreach (var value in values)
+ {
+ // Clients write these in both the dashed and the plain form, so compare them parsed.
+ if (!Guid.TryParse(value, out var parsed))
+ {
+ rewritten.Add(value);
+ continue;
+ }
+
+ var isReplaced = map.TryGetValue(parsed, out var replacement) && !replacement.Equals(parsed);
+ if (isReplaced)
+ {
+ parsed = replacement;
+ touched = true;
+ }
+
+ if (!seen.Add(parsed))
+ {
+ touched = true;
+ continue;
+ }
+
+ var format = value.Contains('-', StringComparison.Ordinal) ? "D" : "N";
+ rewritten.Add(isReplaced ? parsed.ToString(format, CultureInfo.InvariantCulture) : value);
+ }
+
+ if (touched)
+ {
+ user.SetPreference(kind, rewritten.ToArray());
+ }
+
+ return touched;
}
/// <summary>
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 82de3546f0..ae15c75350 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -484,6 +484,15 @@ namespace MediaBrowser.Controller.Library
string uniqueId);
/// <summary>
+ /// Gets the identifier of a named user view.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="parentId">The parent identifier.</param>
+ /// <param name="viewType">Type of the view.</param>
+ /// <returns>The named view identifier.</returns>
+ Guid GetNamedViewId(User user, Guid parentId, CollectionType? viewType);
+
+ /// <summary>
/// Gets the shadow view.
/// </summary>
/// <param name="parent">The parent.</param>
@@ -496,6 +505,15 @@ namespace MediaBrowser.Controller.Library
string sortName);
/// <summary>
+ /// Gets the identifier of a shadow view.
+ /// </summary>
+ /// <param name="name">The view name.</param>
+ /// <param name="parentId">The parent identifier.</param>
+ /// <param name="viewType">Type of the view.</param>
+ /// <returns>The shadow view identifier.</returns>
+ Guid GetShadowViewId(string name, Guid parentId, CollectionType? viewType);
+
+ /// <summary>
/// Gets the season number from path.
/// </summary>
/// <param name="path">The path.</param>
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs
index 79b9d1e2c5..3d0840b776 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs
@@ -1,12 +1,19 @@
using System;
+using System.Globalization;
+using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
+using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.LibraryStructureDto;
+using Jellyfin.Data;
+using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
@@ -218,4 +225,122 @@ public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinAppl
using var response = await client.DeleteAsync("Library/VirtualFolders?name=test&refreshLibrary=true", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
}
+
+ [Fact]
+ [Priority(1)]
+ public async Task RenameLibrary_UserHasLibraryPreferences_RemapsLibraryId()
+ {
+ const string Name = "rename-prefs-old";
+ const string NewName = "rename-prefs-new";
+
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
+
+ var libraryManager = _factory.Services.GetRequiredService<ILibraryManager>();
+ var userManager = _factory.Services.GetRequiredService<IUserManager>();
+ var rootFolderPath = _factory.Services.GetRequiredService<IServerApplicationPaths>().DefaultUserViewsPath;
+
+ var body = new AddVirtualFolderDto() { LibraryOptions = new LibraryOptions() };
+ using var createResponse = await client.PostAsJsonAsync($"Library/VirtualFolders?name={Name}&collectionType=movies&refreshLibrary=true", body, _jsonOptions, TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.NoContent, createResponse.StatusCode);
+
+ var oldId = libraryManager.GetNewItemId(Path.Combine(rootFolderPath, Name), typeof(CollectionFolder));
+ var newId = libraryManager.GetNewItemId(Path.Combine(rootFolderPath, NewName), typeof(CollectionFolder));
+ Assert.NotEqual(oldId, newId);
+
+ var oldLibrary = libraryManager.GetUserRootFolder().Children.OfType<CollectionFolder>().Single(f => f.Id.Equals(oldId));
+
+ var otherId = Guid.NewGuid();
+ var user = await userManager.CreateUserAsync("rename-prefs-user");
+ user.SetPermission(PermissionKind.EnableAllFolders, false);
+ user.SetPreference(PreferenceKind.EnabledFolders, [otherId, oldId, newId]);
+ user.SetPreference(PreferenceKind.BlockedMediaFolders, [oldId]);
+ user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, [oldId, otherId]);
+ user.SetPreference(PreferenceKind.LatestItemExcludes, [oldId]);
+
+ // The views built from the library take its id and name as input, so they move with it.
+ var oldShadowId = libraryManager.GetShadowView(oldLibrary, oldLibrary.CollectionType, string.Empty).Id;
+ var oldUserViewId = libraryManager.GetNamedView(user, oldLibrary.Name, oldId, oldLibrary.CollectionType, string.Empty).Id;
+ user.SetPreference(PreferenceKind.MyMediaExcludes, [otherId.ToString(), oldId.ToString(), oldShadowId.ToString("N", CultureInfo.InvariantCulture)]);
+ user.SetPreference(PreferenceKind.GroupedFolders, [oldId]);
+ user.SetPreference(PreferenceKind.OrderedViews, [otherId.ToString(), oldUserViewId.ToString(), oldShadowId.ToString(), oldId.ToString()]);
+ user.SetPreference(PreferenceKind.BlockedTags, ["keep"]);
+ await userManager.UpdateUserAsync(user);
+
+ using var renameResponse = await client.PostAsync(
+ $"Library/VirtualFolders/Name?name={Name}&newName={NewName}&refreshLibrary=true",
+ null,
+ TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.NoContent, renameResponse.StatusCode);
+
+ var reloaded = userManager.GetUserById(user.Id)!;
+ Assert.Equal([otherId, newId], reloaded.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders));
+ Assert.Equal([newId], reloaded.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders));
+ Assert.Equal([newId, otherId], reloaded.GetPreferenceValues<Guid>(PreferenceKind.EnableContentDeletionFromFolders));
+ Assert.Equal([newId], reloaded.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes));
+ // The endpoint refreshes in the background; make sure the renamed library has been picked up.
+ await libraryManager.ValidateTopLibraryFolders(CancellationToken.None, true);
+ var newLibrary = libraryManager.GetUserRootFolder().Children.OfType<CollectionFolder>().Single(f => f.Id.Equals(newId));
+ var newShadowId = libraryManager.GetShadowView(newLibrary, newLibrary.CollectionType, string.Empty).Id;
+ var newUserViewId = libraryManager.GetNamedView(reloaded, newLibrary.Name, newId, newLibrary.CollectionType, string.Empty).Id;
+ Assert.NotEqual(oldShadowId, newShadowId);
+ Assert.NotEqual(oldUserViewId, newUserViewId);
+ Assert.Equal(
+ [otherId.ToString(), newId.ToString(), newShadowId.ToString("N", CultureInfo.InvariantCulture)],
+ reloaded.GetPreference(PreferenceKind.MyMediaExcludes));
+ Assert.Equal([newId], reloaded.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders));
+ Assert.Equal(
+ [otherId.ToString(), newUserViewId.ToString(), newShadowId.ToString(), newId.ToString()],
+ reloaded.GetPreference(PreferenceKind.OrderedViews));
+ Assert.Equal(["keep"], reloaded.GetPreference(PreferenceKind.BlockedTags));
+
+ // With the block list cleared, the enabled list alone must let the user see the renamed library.
+ reloaded.SetPreference(PreferenceKind.BlockedMediaFolders, Array.Empty<Guid>());
+ await userManager.UpdateUserAsync(reloaded);
+ Assert.True(newLibrary.IsVisible(userManager.GetUserById(user.Id)!));
+
+ using var cleanup = await client.DeleteAsync($"Library/VirtualFolders?name={NewName}&refreshLibrary=false", TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.NoContent, cleanup.StatusCode);
+ }
+
+ [Fact]
+ [Priority(1)]
+ public async Task RenameLibrary_CapitalizationOnly_LeavesPreferencesAlone()
+ {
+ const string Name = "rename-case-test";
+
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
+
+ var libraryManager = _factory.Services.GetRequiredService<ILibraryManager>();
+ var userManager = _factory.Services.GetRequiredService<IUserManager>();
+ var rootFolderPath = _factory.Services.GetRequiredService<IServerApplicationPaths>().DefaultUserViewsPath;
+
+ var body = new AddVirtualFolderDto() { LibraryOptions = new LibraryOptions() };
+ using var createResponse = await client.PostAsJsonAsync($"Library/VirtualFolders?name={Name}&refreshLibrary=false", body, _jsonOptions, TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.NoContent, createResponse.StatusCode);
+
+ var oldId = libraryManager.GetNewItemId(Path.Combine(rootFolderPath, Name), typeof(CollectionFolder));
+ var newId = libraryManager.GetNewItemId(Path.Combine(rootFolderPath, Name.ToUpperInvariant()), typeof(CollectionFolder));
+ var oldLibrary = libraryManager.GetUserRootFolder().Children.OfType<CollectionFolder>().Single(f => f.Id.Equals(oldId));
+ var oldShadowId = libraryManager.GetShadowView(oldLibrary, oldLibrary.CollectionType, string.Empty).Id;
+ var newShadowId = libraryManager.GetShadowViewId(Name.ToUpperInvariant(), newId, oldLibrary.CollectionType);
+
+ var user = await userManager.CreateUserAsync("rename-case-user");
+ user.SetPreference(PreferenceKind.EnabledFolders, [oldId]);
+ user.SetPreference(PreferenceKind.MyMediaExcludes, [oldShadowId]);
+ await userManager.UpdateUserAsync(user);
+
+ using var renameResponse = await client.PostAsync(
+ $"Library/VirtualFolders/Name?name={Name}&newName={Name.ToUpperInvariant()}&refreshLibrary=false",
+ null,
+ TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.NoContent, renameResponse.StatusCode);
+
+ Assert.Equal([newId], userManager.GetUserById(user.Id)!.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders));
+ Assert.Equal([newShadowId], userManager.GetUserById(user.Id)!.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes));
+
+ using var cleanup = await client.DeleteAsync($"Library/VirtualFolders?name={Name.ToUpperInvariant()}&refreshLibrary=false", TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.NoContent, cleanup.StatusCode);
+ }
}