Browse Source

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

cvium 4 years ago
parent
commit
b99519898d

+ 23 - 5
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -2403,11 +2403,11 @@ namespace Emby.Server.Implementations.Data
 
                 if (string.IsNullOrEmpty(item.OfficialRating))
                 {
-                    builder.Append("((OfficialRating is null) * 10)");
+                    builder.Append("(OfficialRating is null * 10)");
                 }
                 else
                 {
-                    builder.Append("((OfficialRating=@ItemOfficialRating) * 10)");
+                    builder.Append("(OfficialRating=@ItemOfficialRating * 10)");
                 }
 
                 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 )");
                 }
 
-                //// genres, tags
-                builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId)) * 10)");
+                // genres, tags, studios, person, year?
+                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");
 
@@ -5052,7 +5070,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             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);
 

+ 0 - 135
Jellyfin.Api/Controllers/AlbumsController.cs

@@ -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);
-        }
-    }
-}

+ 64 - 94
Jellyfin.Api/Controllers/LibraryController.cs

@@ -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>
         /// <response code="200">Similar items returned.</response>
         /// <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("Albums/{itemId}/Similar", Name = "GetSimilarAlbums2")]
-        [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows2")]
-        [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies2")]
-        [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")]
+        [HttpGet("Albums/{itemId}/Similar")]
+        [HttpGet("Shows/{itemId}/Similar")]
+        [HttpGet("Movies/{itemId}/Similar")]
+        [HttpGet("Trailers/{itemId}/Similar")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
@@ -701,33 +701,72 @@ namespace Jellyfin.Api.Controllers
                     : _libraryManager.RootFolder)
                 : _libraryManager.GetItemById(itemId);
 
+            if (item is Episode || (item is IItemByName && !(item is MusicArtist)))
+            {
+                return new QueryResult<BaseItemDto>();
+            }
+
+            var user = userId.HasValue && !userId.Equals(Guid.Empty)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
+            var dtoOptions = new DtoOptions()
+                .AddItemFields(fields)
+                .AddClientFields(Request);
+
             var program = item as IHasProgramAttributes;
-            var isMovie = item is MediaBrowser.Controller.Entities.Movies.Movie || (program != null && program.IsMovie) || item is Trailer;
-            if (program != null && program.IsSeries)
+            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)
             {
-                return GetSimilarItemsResult(
-                    item,
-                    excludeArtistIds,
-                    userId,
-                    limit,
-                    fields,
-                    new[] { nameof(Series) },
-                    false);
+                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);
             }
 
-            if (item is MediaBrowser.Controller.Entities.TV.Episode || (item is IItemByName && !(item is MusicArtist)))
+            var query = new InternalItemsQuery(user)
             {
-                return new QueryResult<BaseItemDto>();
+                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);
             }
 
-            return GetSimilarItemsResult(
-                item,
-                excludeArtistIds,
-                userId,
-                limit,
-                fields,
-                new[] { item.GetType().Name },
-                isMovie);
+            List<BaseItem> itemsResult = _libraryManager.GetItemList(query);
+
+            var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
+
+            return new QueryResult<BaseItemDto>
+            {
+                Items = returnList,
+                TotalRecordCount = itemsResult.Count
+            };
         }
 
         /// <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)
         {
             return contentType switch

+ 0 - 182
Jellyfin.Api/Helpers/SimilarItemsHelper.cs

@@ -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;
-        }
-    }
-}