From 64befbcdaa2718468fc015cc8224c8d71ddd001c Mon Sep 17 00:00:00 2001 From: orut34iop Date: Sun, 27 Sep 2026 16:30:32 -0400 Subject: Backport pull request #18005 from jellyfin/release-12.z Match artist creation lookups using CleanName Original-merge: d341176eef63e38839f648b00a358240279ec363 Merged-by: crobibero Backported-by: Cody Robibero --- .../Item/ArtistNameLookupTests.cs | 173 +++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 tests/Jellyfin.Server.Implementations.Tests/Item/ArtistNameLookupTests.cs (limited to 'tests') diff --git a/tests/Jellyfin.Server.Implementations.Tests/Item/ArtistNameLookupTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Item/ArtistNameLookupTests.cs new file mode 100644 index 0000000000..0c1d453293 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Item/ArtistNameLookupTests.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using AutoFixture; +using AutoFixture.AutoMoq; +using Emby.Naming.Common; +using Emby.Server.Implementations.Data; +using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Extensions; +using Jellyfin.Server.Implementations.Item; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Configuration; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Moq; +using Xunit; +using ServerLibraryManager = Emby.Server.Implementations.Library.LibraryManager; + +namespace Jellyfin.Server.Implementations.Tests.Item; + +public sealed class ArtistNameLookupTests : SqliteDbTestFixture +{ + private readonly CommandRecorder _recorder; + + public ArtistNameLookupTests() + : this(new CommandRecorder()) + { + } + + private ArtistNameLookupTests(CommandRecorder recorder) + : base(recorder) + { + _recorder = recorder; + } + + [Theory] + [InlineData("Björk", "bjork")] + [InlineData("AC/DC", "ac dc")] + [InlineData("An Artist", " AN ARTIST ")] + [InlineData("Artist", "ARTIST")] + public void GetArtist_UsesSameNormalizedNameAsFindArtists(string storedName, string requestedName) + { + var lookup = new ItemTypeLookup(); + var artistId = Guid.NewGuid(); + using (var context = CreateDbContext()) + { + context.BaseItems.AddRange( + new BaseItemEntity + { + Id = artistId, + Name = storedName, + CleanName = storedName.GetCleanValue(), + Type = lookup.BaseItemKindNames[BaseItemKind.MusicArtist] + }, + new BaseItemEntity + { + Id = Guid.NewGuid(), + Name = storedName, + CleanName = storedName.GetCleanValue(), + Type = lookup.BaseItemKindNames[BaseItemKind.Person] + }); + context.SaveChanges(); + } + + var manager = CreateLibraryManager(lookup); + _recorder.Commands.Clear(); + + Assert.Equal(artistId, manager.GetArtist(requestedName).Id); + var query = Assert.Single(_recorder.Commands, c => c.Sql.Contains("\"CleanName\" =", StringComparison.Ordinal)); + Assert.Contains(Explain(query), line => line.Contains("IX_BaseItems_Type_CleanName (Type=? AND CleanName=?)", StringComparison.Ordinal)); + Assert.Equal(artistId, Assert.Single(manager.GetArtists([requestedName])[requestedName]).Id); + } + + [Fact] + public void GetArtist_PrefersFilesystemArtistWhenNormalizedNamesMatch() + { + var lookup = new ItemTypeLookup(); + var parentId = Guid.NewGuid(); + var artistId = Guid.NewGuid(); + using (var context = CreateDbContext()) + { + context.BaseItems.AddRange( + new BaseItemEntity + { + Id = parentId, + Name = "Music", + Type = lookup.BaseItemKindNames[BaseItemKind.Folder] + }, + new BaseItemEntity + { + Id = Guid.NewGuid(), + Name = "Bjork", + CleanName = "Bjork".GetCleanValue(), + Type = lookup.BaseItemKindNames[BaseItemKind.MusicArtist] + }, + new BaseItemEntity + { + Id = artistId, + ParentId = parentId, + Name = "Björk", + CleanName = "Björk".GetCleanValue(), + Type = lookup.BaseItemKindNames[BaseItemKind.MusicArtist] + }); + context.SaveChanges(); + } + + Assert.Equal(artistId, CreateLibraryManager(lookup).GetArtist("Bjork").Id); + } + + private ServerLibraryManager CreateLibraryManager(ItemTypeLookup lookup) + { + var repository = CreateBaseItemRepository(lookup); + var fixture = new Fixture().Customize(new AutoMoqCustomization()); + fixture.Register(() => new NamingOptions()); + var configuration = fixture.Freeze>(); + configuration.Setup(c => c.Configuration).Returns(new ServerConfiguration()); + configuration.Setup(c => c.ApplicationPaths.ProgramDataPath).Returns("/data"); + fixture.Inject(repository); + fixture.Inject(new LinkedChildrenService(CreateDbContextFactory(), lookup, repository)); + return fixture.Create(); + } + + private string[] Explain(RecordedCommand query) + { + using var context = CreateDbContext(); + using var command = context.Database.GetDbConnection().CreateCommand(); +#pragma warning disable CA2100 // query.Sql is generated by EF Core; query values remain bound parameters. + command.CommandText = "EXPLAIN QUERY PLAN " + query.Sql; +#pragma warning restore CA2100 + foreach (var value in query.Parameters) + { + var parameter = command.CreateParameter(); + parameter.ParameterName = value.Name; + parameter.Value = value.Value; + command.Parameters.Add(parameter); + } + + using var reader = command.ExecuteReader(); + var plan = new List(); + while (reader.Read()) + { + plan.Add(reader.GetString(3)); + } + + return plan.ToArray(); + } + + private sealed record RecordedCommand(string Sql, (string Name, object? Value)[] Parameters); + + private sealed class CommandRecorder : DbCommandInterceptor + { + public List Commands { get; } = []; + + public override InterceptionResult ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult result) + { + Record(command); + return result; + } + + public override InterceptionResult NonQueryExecuting(DbCommand command, CommandEventData eventData, InterceptionResult result) + { + Record(command); + return result; + } + + private void Record(DbCommand command) => Commands.Add(new RecordedCommand( + command.CommandText, + command.Parameters.Cast().Select(p => (p.ParameterName, p.Value)).ToArray())); + } +} -- cgit v1.2.3