Merge pull request #17715 from Shadowghost/fix-people-cleanup

Delete credits nothing maps to and bound item-by-name folder names
This commit is contained in:
Cody Robibero
2026-08-25 18:28:52 -04:00
committed by GitHub
14 changed files with 231 additions and 25 deletions
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations.Entities;
@@ -27,6 +28,57 @@ namespace Jellyfin.Controller.Tests.Entities;
public class BaseItemTests
{
[Fact]
public void GetItemByNameFolderName_ShortName_IsKeptAsIs()
{
SetupPassThroughFileSystem();
Assert.Equal("Mairghread Scott", BaseItem.GetItemByNameFolderName("Mairghread Scott."));
}
[Fact]
public void GetItemByNameFolderName_OverlongName_FitsInAPathComponent()
{
SetupPassThroughFileSystem();
// What a provider result that concatenated a whole credit list into one name looks like.
var name = string.Join(", ", Enumerable.Repeat("Jerry Siegel (created by: Superman)", 20));
var folderName = BaseItem.GetItemByNameFolderName(name);
Assert.True(Encoding.UTF8.GetByteCount(folderName) <= 128);
Assert.StartsWith("Jerry Siegel (created by: Superman)", folderName, StringComparison.Ordinal);
}
[Fact]
public void GetItemByNameFolderName_OverlongNamesSharingAPrefix_StayApart()
{
SetupPassThroughFileSystem();
var prefix = new string('a', 200);
Assert.NotEqual(
BaseItem.GetItemByNameFolderName(prefix + "Joe Shuster"),
BaseItem.GetItemByNameFolderName(prefix + "Bob Kane"));
}
[Fact]
public void GetItemByNameFolderName_OverlongName_IsStable()
{
SetupPassThroughFileSystem();
var name = new string('a', 300);
Assert.Equal(BaseItem.GetItemByNameFolderName(name), BaseItem.GetItemByNameFolderName(name));
}
private static void SetupPassThroughFileSystem()
{
var fileSystem = new Mock<IFileSystem>();
fileSystem.Setup(x => x.GetValidFilename(It.IsAny<string>())).Returns((string name) => name);
BaseItem.FileSystem = fileSystem.Object;
}
[Theory]
[InlineData("", "")]
[InlineData("1", "0000000001")]
@@ -142,6 +142,79 @@ public sealed class PeopleRepositoryUpdatePeopleTests : SqliteDbTestFixture
Assert.Equal("Hero", map.Role);
}
[Fact]
public void UpdatePeople_CreditDroppedByTheProvider_LeavesNoCreditRowBehind()
{
_repository.UpdatePeople(_itemId, [
CreatePerson("Person A", PersonKind.Actor, "Hero"),
CreatePerson("Person B", PersonKind.Actor, "Villain")
]);
_repository.UpdatePeople(_itemId, [CreatePerson("Person A", PersonKind.Actor, "Hero")]);
using var ctx = CreateDbContext();
Assert.Equal(["Person A"], ctx.Peoples.Select(e => e.Name).ToArray());
}
[Fact]
public void UpdatePeople_CreditStillHeldByAnotherItem_IsKept()
{
_repository.UpdatePeople(_itemId, [CreatePerson("Person A", PersonKind.Actor, "Hero")]);
_repository.UpdatePeople(AddMovie("Other Movie"), [CreatePerson("Person A", PersonKind.Actor, "Hero")]);
_repository.UpdatePeople(_itemId, []);
using var after = CreateDbContext();
Assert.Single(after.Peoples);
Assert.Single(after.PeopleBaseItemMap);
}
[Fact]
public void DeleteOrphanedCredits_CreditNoItemMapsTo_IsDeleted()
{
_repository.UpdatePeople(_itemId, [CreatePerson("Person A", PersonKind.Actor, "Hero")]);
using (var ctx = CreateDbContext())
{
// The state a credit was left in before UpdatePeople cleaned up after itself.
ctx.PeopleBaseItemMap.RemoveRange(ctx.PeopleBaseItemMap);
ctx.SaveChanges();
}
Assert.Equal(1, _repository.DeleteOrphanedCredits());
using var after = CreateDbContext();
Assert.Empty(after.Peoples);
}
[Fact]
public void DeleteOrphanedCredits_CreditAnItemMapsTo_IsKept()
{
_repository.UpdatePeople(_itemId, [CreatePerson("Person A", PersonKind.Actor, "Hero")]);
Assert.Equal(0, _repository.DeleteOrphanedCredits());
using var after = CreateDbContext();
Assert.Single(after.Peoples);
}
private Guid AddMovie(string name)
{
var id = Guid.NewGuid();
using var ctx = CreateDbContext();
ctx.BaseItems.Add(new BaseItemEntity
{
Id = id,
Type = new ItemTypeLookup().BaseItemKindNames[BaseItemKind.Movie],
Name = name,
MediaType = "Video",
IsMovie = true,
IsFolder = false,
IsVirtualItem = false
});
ctx.SaveChanges();
return id;
}
private static PersonInfo CreatePerson(string name, PersonKind type, string role)
{
return new PersonInfo