Remove duplicate /Similar endpoints and add poor matching for artists and albums

This commit is contained in:
cvium 2020-11-09 20:15:15 +01:00
parent 96dcd9c87e
commit b99519898d
4 changed files with 90 additions and 419 deletions

View File

@ -2403,11 +2403,11 @@ namespace Emby.Server.Implementations.Data
if (string.IsNullOrEmpty(item.OfficialRating)) if (string.IsNullOrEmpty(item.OfficialRating))
{ {
builder.Append("((OfficialRating is null) * 10)"); builder.Append("(OfficialRating is null * 10)");
} }
else else
{ {
builder.Append("((OfficialRating=@ItemOfficialRating) * 10)"); builder.Append("(OfficialRating=@ItemOfficialRating * 10)");
} }
if (item.ProductionYear.HasValue) if (item.ProductionYear.HasValue)
@ -2416,8 +2416,26 @@ namespace Emby.Server.Implementations.Data
builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 5 Else 0 End )"); builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 5 Else 0 End )");
} }
//// genres, tags // genres, tags, studios, person, year?
builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId)) * 10)"); builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId))");
if (item is MusicArtist)
{
// Match albums where the artist is AlbumArtist against other albums.
// It is assumed that similar albums => similar artists.
builder.Append(
@"+ (WITH artistValues AS (
SELECT DISTINCT albumValues.CleanValue
FROM ItemValues albumValues
INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = @SimilarItemId
), similarArtist AS (
SELECT albumValues.ItemId
FROM ItemValues albumValues
INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = A.Guid
) SELECT COUNT(DISTINCT(CleanValue)) * 10 FROM ItemValues WHERE ItemId IN (SELECT ItemId FROM similarArtist) AND CleanValue IN (SELECT CleanValue FROM artistValues))");
}
builder.Append(") as SimilarityScore"); builder.Append(") as SimilarityScore");
@ -5052,7 +5070,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
CheckDisposed(); CheckDisposed();
var commandText = "select ItemId, Name, Role, PersonType, SortOrder from People"; var commandText = "select ItemId, Name, Role, PersonType, SortOrder from People p";
var whereClauses = GetPeopleWhereClauses(query, null); var whereClauses = GetPeopleWhereClauses(query, null);

View File

@ -1,135 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// The albums controller.
/// </summary>
[Route("")]
public class AlbumsController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService;
/// <summary>
/// Initializes a new instance of the <see cref="AlbumsController"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
public AlbumsController(
IUserManager userManager,
ILibraryManager libraryManager,
IDtoService dtoService)
{
_userManager = userManager;
_libraryManager = libraryManager;
_dtoService = dtoService;
}
/// <summary>
/// Finds albums similar to a given album.
/// </summary>
/// <param name="albumId">The album id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="excludeArtistIds">Optional. Ids of artists to exclude.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <response code="200">Similar albums returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar albums.</returns>
[HttpGet("Albums/{albumId}/Similar")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarAlbums(
[FromRoute, Required] string albumId,
[FromQuery] Guid? userId,
[FromQuery] string? excludeArtistIds,
[FromQuery] int? limit)
{
var dtoOptions = new DtoOptions().AddClientFields(Request);
return SimilarItemsHelper.GetSimilarItemsResult(
dtoOptions,
_userManager,
_libraryManager,
_dtoService,
userId,
albumId,
excludeArtistIds,
limit,
new[] { typeof(MusicAlbum) },
GetAlbumSimilarityScore);
}
/// <summary>
/// Finds artists similar to a given artist.
/// </summary>
/// <param name="artistId">The artist id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="excludeArtistIds">Optional. Ids of artists to exclude.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <response code="200">Similar artists returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar artists.</returns>
[HttpGet("Artists/{artistId}/Similar")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarArtists(
[FromRoute, Required] string artistId,
[FromQuery] Guid? userId,
[FromQuery] string? excludeArtistIds,
[FromQuery] int? limit)
{
var dtoOptions = new DtoOptions().AddClientFields(Request);
return SimilarItemsHelper.GetSimilarItemsResult(
dtoOptions,
_userManager,
_libraryManager,
_dtoService,
userId,
artistId,
excludeArtistIds,
limit,
new[] { typeof(MusicArtist) },
SimilarItemsHelper.GetSimiliarityScore);
}
/// <summary>
/// Gets a similairty score of two albums.
/// </summary>
/// <param name="item1">The first item.</param>
/// <param name="item1People">The item1 people.</param>
/// <param name="allPeople">All people.</param>
/// <param name="item2">The second item.</param>
/// <returns>System.Int32.</returns>
private int GetAlbumSimilarityScore(BaseItem item1, List<PersonInfo> item1People, List<PersonInfo> allPeople, BaseItem item2)
{
var points = SimilarItemsHelper.GetSimiliarityScore(item1, item1People, allPeople, item2);
var album1 = (MusicAlbum)item1;
var album2 = (MusicAlbum)item2;
var artists1 = album1
.GetAllArtists()
.DistinctNames()
.ToList();
var artists2 = new HashSet<string>(
album2.GetAllArtists().DistinctNames(),
StringComparer.OrdinalIgnoreCase);
return points + artists1.Where(artists2.Contains).Sum(i => 5);
}
}
}

View File

@ -680,12 +680,12 @@ namespace Jellyfin.Api.Controllers
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
/// <response code="200">Similar items returned.</response> /// <response code="200">Similar items returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
[HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists2")] [HttpGet("Artists/{itemId}/Similar")]
[HttpGet("Items/{itemId}/Similar")] [HttpGet("Items/{itemId}/Similar")]
[HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums2")] [HttpGet("Albums/{itemId}/Similar")]
[HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows2")] [HttpGet("Shows/{itemId}/Similar")]
[HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies2")] [HttpGet("Movies/{itemId}/Similar")]
[HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")] [HttpGet("Trailers/{itemId}/Similar")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems( public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
@ -701,33 +701,72 @@ namespace Jellyfin.Api.Controllers
: _libraryManager.RootFolder) : _libraryManager.RootFolder)
: _libraryManager.GetItemById(itemId); : _libraryManager.GetItemById(itemId);
var program = item as IHasProgramAttributes; if (item is Episode || (item is IItemByName && !(item is MusicArtist)))
var isMovie = item is MediaBrowser.Controller.Entities.Movies.Movie || (program != null && program.IsMovie) || item is Trailer;
if (program != null && program.IsSeries)
{
return GetSimilarItemsResult(
item,
excludeArtistIds,
userId,
limit,
fields,
new[] { nameof(Series) },
false);
}
if (item is MediaBrowser.Controller.Entities.TV.Episode || (item is IItemByName && !(item is MusicArtist)))
{ {
return new QueryResult<BaseItemDto>(); return new QueryResult<BaseItemDto>();
} }
return GetSimilarItemsResult( var user = userId.HasValue && !userId.Equals(Guid.Empty)
item, ? _userManager.GetUserById(userId.Value)
excludeArtistIds, : null;
userId, var dtoOptions = new DtoOptions()
limit, .AddItemFields(fields)
fields, .AddClientFields(Request);
new[] { item.GetType().Name },
isMovie); var program = item as IHasProgramAttributes;
bool? isMovie = item is Movie || (program != null && program.IsMovie) || item is Trailer;
bool? isSeries = item is Series || (program != null && program.IsSeries);
var includeItemTypes = new List<string>();
if (isMovie.Value)
{
includeItemTypes.Add(nameof(Movie));
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
includeItemTypes.Add(nameof(Trailer));
includeItemTypes.Add(nameof(LiveTvProgram));
}
}
else if (isSeries.Value)
{
includeItemTypes.Add(nameof(Series));
}
else
{
// For non series and movie types these columns are typically null
isSeries = null;
isMovie = null;
includeItemTypes.Add(item.GetType().Name);
}
var query = new InternalItemsQuery(user)
{
Limit = limit,
IncludeItemTypes = includeItemTypes.ToArray(),
IsMovie = isMovie,
IsSeries = isSeries,
SimilarTo = item,
DtoOptions = dtoOptions,
EnableTotalRecordCount = !isMovie ?? true,
EnableGroupByMetadataKey = isMovie ?? false,
MinSimilarityScore = 2 // A remnant from album/artist scoring
};
// ExcludeArtistIds
if (!string.IsNullOrEmpty(excludeArtistIds))
{
query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
}
List<BaseItem> itemsResult = _libraryManager.GetItemList(query);
var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
return new QueryResult<BaseItemDto>
{
Items = returnList,
TotalRecordCount = itemsResult.Count
};
} }
/// <summary> /// <summary>
@ -880,75 +919,6 @@ namespace Jellyfin.Api.Controllers
} }
} }
private QueryResult<BaseItemDto> GetSimilarItemsResult(
BaseItem item,
string? excludeArtistIds,
Guid? userId,
int? limit,
string? fields,
string[] includeItemTypes,
bool isMovie)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
: null;
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request);
var query = new InternalItemsQuery(user)
{
Limit = limit,
IncludeItemTypes = includeItemTypes,
IsMovie = isMovie,
SimilarTo = item,
DtoOptions = dtoOptions,
EnableTotalRecordCount = !isMovie,
EnableGroupByMetadataKey = isMovie
};
// ExcludeArtistIds
if (!string.IsNullOrEmpty(excludeArtistIds))
{
query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
}
List<BaseItem> itemsResult;
if (isMovie)
{
var itemTypes = new List<string> { nameof(MediaBrowser.Controller.Entities.Movies.Movie) };
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
itemTypes.Add(nameof(Trailer));
itemTypes.Add(nameof(LiveTvProgram));
}
query.IncludeItemTypes = itemTypes.ToArray();
itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList();
}
else if (item is MusicArtist)
{
query.IncludeItemTypes = Array.Empty<string>();
itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList();
}
else
{
itemsResult = _libraryManager.GetItemList(query);
}
var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
var result = new QueryResult<BaseItemDto>
{
Items = returnList,
TotalRecordCount = itemsResult.Count
};
return result;
}
private static string[] GetRepresentativeItemTypes(string? contentType) private static string[] GetRepresentativeItemTypes(string? contentType)
{ {
return contentType switch return contentType switch

View File

@ -1,182 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
namespace Jellyfin.Api.Helpers
{
/// <summary>
/// The similar items helper class.
/// </summary>
public static class SimilarItemsHelper
{
internal static QueryResult<BaseItemDto> GetSimilarItemsResult(
DtoOptions dtoOptions,
IUserManager userManager,
ILibraryManager libraryManager,
IDtoService dtoService,
Guid? userId,
string id,
string? excludeArtistIds,
int? limit,
Type[] includeTypes,
Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? userManager.GetUserById(userId.Value)
: null;
var item = string.IsNullOrEmpty(id) ?
(!userId.Equals(Guid.Empty) ? libraryManager.GetUserRootFolder() :
libraryManager.RootFolder) : libraryManager.GetItemById(id);
var query = new InternalItemsQuery(user)
{
IncludeItemTypes = includeTypes.Select(i => i.Name).ToArray(),
Recursive = true,
DtoOptions = dtoOptions,
ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds)
};
var inputItems = libraryManager.GetItemList(query);
var items = GetSimilaritems(item, libraryManager, inputItems, getSimilarityScore)
.ToList();
var returnItems = items;
if (limit.HasValue && limit < returnItems.Count)
{
returnItems = returnItems.GetRange(0, limit.Value);
}
var dtos = dtoService.GetBaseItemDtos(returnItems, dtoOptions, user);
return new QueryResult<BaseItemDto>
{
Items = dtos,
TotalRecordCount = items.Count
};
}
/// <summary>
/// Gets the similaritems.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="inputItems">The input items.</param>
/// <param name="getSimilarityScore">The get similarity score.</param>
/// <returns>IEnumerable{BaseItem}.</returns>
private static IEnumerable<BaseItem> GetSimilaritems(
BaseItem item,
ILibraryManager libraryManager,
IEnumerable<BaseItem> inputItems,
Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore)
{
var itemId = item.Id;
inputItems = inputItems.Where(i => i.Id != itemId);
var itemPeople = libraryManager.GetPeople(item);
var allPeople = libraryManager.GetPeople(new InternalPeopleQuery
{
AppearsInItemId = item.Id
});
return inputItems.Select(i => new Tuple<BaseItem, int>(i, getSimilarityScore(item, itemPeople, allPeople, i)))
.Where(i => i.Item2 > 2)
.OrderByDescending(i => i.Item2)
.Select(i => i.Item1);
}
private static IEnumerable<string> GetTags(BaseItem item)
{
return item.Tags;
}
/// <summary>
/// Gets the similiarity score.
/// </summary>
/// <param name="item1">The item1.</param>
/// <param name="item1People">The item1 people.</param>
/// <param name="allPeople">All people.</param>
/// <param name="item2">The item2.</param>
/// <returns>System.Int32.</returns>
internal static int GetSimiliarityScore(BaseItem item1, List<PersonInfo> item1People, List<PersonInfo> allPeople, BaseItem item2)
{
var points = 0;
if (!string.IsNullOrEmpty(item1.OfficialRating) && string.Equals(item1.OfficialRating, item2.OfficialRating, StringComparison.OrdinalIgnoreCase))
{
points += 10;
}
// Find common genres
points += item1.Genres.Where(i => item2.Genres.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10);
// Find common tags
points += GetTags(item1).Where(i => GetTags(item2).Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10);
// Find common studios
points += item1.Studios.Where(i => item2.Studios.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 3);
var item2PeopleNames = allPeople.Where(i => i.ItemId == item2.Id)
.Select(i => i.Name)
.Where(i => !string.IsNullOrWhiteSpace(i))
.DistinctNames()
.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
points += item1People.Where(i => item2PeopleNames.ContainsKey(i.Name)).Sum(i =>
{
if (string.Equals(i.Type, PersonType.Director, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Director, StringComparison.OrdinalIgnoreCase))
{
return 5;
}
if (string.Equals(i.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Actor, StringComparison.OrdinalIgnoreCase))
{
return 3;
}
if (string.Equals(i.Type, PersonType.Composer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Composer, StringComparison.OrdinalIgnoreCase))
{
return 3;
}
if (string.Equals(i.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
{
return 3;
}
if (string.Equals(i.Type, PersonType.Writer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Writer, StringComparison.OrdinalIgnoreCase))
{
return 2;
}
return 1;
});
if (item1.ProductionYear.HasValue && item2.ProductionYear.HasValue)
{
var diff = Math.Abs(item1.ProductionYear.Value - item2.ProductionYear.Value);
// Add if they came out within the same decade
if (diff < 10)
{
points += 2;
}
// And more if within five years
if (diff < 5)
{
points += 2;
}
}
return points;
}
}
}