Files
jellyfin-ha-src/tests/Jellyfin.Server.Implementations.Tests/Library/MovieSimilarItemsProviderTests.cs
2026-09-06 17:20:18 +02:00

156 lines
5.6 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Data;
using Emby.Server.Implementations.Library.SimilarItems;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Server.Implementations.Tests.Item;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Configuration;
using Moq;
using Xunit;
using BaseItemKind = Jellyfin.Data.Enums.BaseItemKind;
namespace Jellyfin.Server.Implementations.Tests.Library;
/// <summary>
/// Covers how <see cref="MovieSimilarItemsProvider"/> treats alternate versions: they share their
/// primary's genres, tags, studios and people, so they score like it and must not be offered as
/// something similar - neither as another copy of a recommendation nor as a match for the source.
/// </summary>
public sealed class MovieSimilarItemsProviderTests : SqliteDbTestFixture
{
private readonly MovieSimilarItemsProvider _provider;
private readonly User _user = new("test", "auth-provider", "reset-provider");
private readonly string _movieTypeName;
private readonly Guid _source = Guid.NewGuid();
private readonly Guid _sourceAlternate = Guid.NewGuid();
private readonly Guid _similar = Guid.NewGuid();
private readonly Guid _similarAlternate = Guid.NewGuid();
private readonly Guid _unrelated = Guid.NewGuid();
public MovieSimilarItemsProviderTests()
{
var itemTypeLookup = new ItemTypeLookup();
_movieTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie]!;
using (var context = CreateDbContext())
{
Seed(context);
}
var serverConfigurationManager = new Mock<IServerConfigurationManager>();
serverConfigurationManager.Setup(c => c.Configuration).Returns(new ServerConfiguration());
_provider = new MovieSimilarItemsProvider(
CreateDbContextFactory(),
CreateBaseItemRepository(itemTypeLookup),
serverConfigurationManager.Object,
new Mock<ILibraryManager>().Object);
}
[Fact]
public async Task GetSimilarItems_ReturnsThePrimaryAndNeitherVersionOfTheSource()
{
var items = await GetSimilarItemsAsync().ConfigureAwait(true);
Assert.Equal([_similar], items);
}
[Fact]
public async Task GetSimilarItems_DoesNotOfferAnAlternateVersionOfAMatch()
{
var items = await GetSimilarItemsAsync().ConfigureAwait(true);
Assert.DoesNotContain(_similarAlternate, items);
}
[Fact]
public async Task GetSimilarItems_DoesNotOfferTheSourcesOwnOtherVersion()
{
var items = await GetSimilarItemsAsync().ConfigureAwait(true);
Assert.DoesNotContain(_sourceAlternate, items);
}
private async Task<List<Guid>> GetSimilarItemsAsync()
{
var results = await _provider.GetSimilarItemsAsync(
new Movie { Id = _source, Name = "Source" },
new SimilarItemsQuery { User = _user, Limit = 10, DtoOptions = new DtoOptions() },
CancellationToken.None).ConfigureAwait(false);
return results.Select(i => i.Id).ToList();
}
private void Seed(JellyfinDbContext context)
{
// One shared genre, so every movie but the unrelated one scores against the source.
var shared = CreateItemValue("Action", "action");
var other = CreateItemValue("Comedy", "comedy");
var source = AddMovie(context, _source, "Source", primaryVersionId: null);
var sourceAlternate = AddMovie(context, _sourceAlternate, "Source 4K", primaryVersionId: _source);
var similar = AddMovie(context, _similar, "Similar", primaryVersionId: null);
var similarAlternate = AddMovie(context, _similarAlternate, "Similar 4K", primaryVersionId: _similar);
var unrelated = AddMovie(context, _unrelated, "Unrelated", primaryVersionId: null);
context.Users.Add(_user);
context.ItemValues.AddRange(shared, other);
context.ItemValuesMap.AddRange(
CreateMap(source, shared),
CreateMap(sourceAlternate, shared),
CreateMap(similar, shared),
CreateMap(similarAlternate, shared),
CreateMap(unrelated, other));
context.SaveChanges();
}
private BaseItemEntity AddMovie(JellyfinDbContext context, Guid id, string name, Guid? primaryVersionId)
{
var item = new BaseItemEntity
{
Id = id,
Type = _movieTypeName,
Name = name,
SortName = name,
MediaType = "Video",
IsMovie = true,
IsFolder = false,
IsVirtualItem = false,
// An alternate presents under its primary's key, which is what collapses the group in listings.
PresentationUniqueKey = (primaryVersionId ?? id).ToString("N"),
PrimaryVersionId = primaryVersionId
};
context.BaseItems.Add(item);
return item;
}
private static ItemValue CreateItemValue(string value, string cleanValue)
=> new()
{
ItemValueId = Guid.NewGuid(),
Type = ItemValueType.Genre,
Value = value,
CleanValue = cleanValue
};
private static ItemValueMap CreateMap(BaseItemEntity item, ItemValue itemValue)
=> new()
{
ItemId = item.Id,
ItemValueId = itemValue.ItemValueId,
Item = item,
ItemValue = itemValue
};
}