浏览代码

unwrapped similar items api into separate endpoints for each type

Luke Pulverenti 12 年之前
父节点
当前提交
9026af7550

+ 87 - 0
MediaBrowser.Api/AlbumsService.cs

@@ -0,0 +1,87 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using ServiceStack.ServiceHost;
+using System;
+using System.Linq;
+
+namespace MediaBrowser.Api
+{
+    [Route("/Albums/{Id}/Similar", "GET")]
+    [Api(Description = "Finds albums similar to a given album.")]
+    public class GetSimilarAlbums : BaseGetSimilarItems
+    {
+    }
+
+    public class AlbumsService : BaseApiService
+    {
+        /// <summary>
+        /// The _user manager
+        /// </summary>
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// The _user data repository
+        /// </summary>
+        private readonly IUserDataRepository _userDataRepository;
+        /// <summary>
+        /// The _library manager
+        /// </summary>
+        private readonly ILibraryManager _libraryManager;
+
+        public AlbumsService(IUserManager userManager, IUserDataRepository userDataRepository, ILibraryManager libraryManager)
+        {
+            _userManager = userManager;
+            _userDataRepository = userDataRepository;
+            _libraryManager = libraryManager;
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetSimilarAlbums request)
+        {
+            var result = SimilarItemsHelper.GetSimilarItems(_userManager,
+                _libraryManager,
+                _userDataRepository,
+                Logger,
+                request, item => item is MusicAlbum,
+                GetAlbumSimilarityScore);
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the album similarity score.
+        /// </summary>
+        /// <param name="item1">The item1.</param>
+        /// <param name="item2">The item2.</param>
+        /// <returns>System.Int32.</returns>
+        private int GetAlbumSimilarityScore(BaseItem item1, BaseItem item2)
+        {
+            var points = SimilarItemsHelper.GetSimiliarityScore(item1, item2);
+
+            var album1 = (MusicAlbum)item1;
+            var album2 = (MusicAlbum)item2;
+
+            var artists1 = album1.RecursiveChildren
+                .OfType<Audio>()
+                .SelectMany(i => new[] { i.AlbumArtist, i.Artist })
+                .Where(i => !string.IsNullOrEmpty(i))
+                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .ToList();
+
+            var artists2 = album2.RecursiveChildren
+                .OfType<Audio>()
+                .SelectMany(i => new[] { i.AlbumArtist, i.Artist })
+                .Where(i => !string.IsNullOrEmpty(i))
+                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .ToList();
+
+            return points + artists1.Where(i => artists2.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 5);
+        }
+    }
+}

+ 66 - 0
MediaBrowser.Api/GamesService.cs

@@ -0,0 +1,66 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using ServiceStack.ServiceHost;
+
+namespace MediaBrowser.Api
+{
+    /// <summary>
+    /// Class GetSimilarGames
+    /// </summary>
+    [Route("/Games/{Id}/Similar", "GET")]
+    [Api(Description = "Finds games similar to a given game.")]
+    public class GetSimilarGames : BaseGetSimilarItems
+    {
+    }
+
+    /// <summary>
+    /// Class GamesService
+    /// </summary>
+    public class GamesService : BaseApiService
+    {
+        /// <summary>
+        /// The _user manager
+        /// </summary>
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// The _user data repository
+        /// </summary>
+        private readonly IUserDataRepository _userDataRepository;
+        /// <summary>
+        /// The _library manager
+        /// </summary>
+        private readonly ILibraryManager _libraryManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="GamesService"/> class.
+        /// </summary>
+        /// <param name="userManager">The user manager.</param>
+        /// <param name="userDataRepository">The user data repository.</param>
+        /// <param name="libraryManager">The library manager.</param>
+        public GamesService(IUserManager userManager, IUserDataRepository userDataRepository, ILibraryManager libraryManager)
+        {
+            _userManager = userManager;
+            _userDataRepository = userDataRepository;
+            _libraryManager = libraryManager;
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetSimilarGames request)
+        {
+            var result = SimilarItemsHelper.GetSimilarItems(_userManager,
+                _libraryManager,
+                _userDataRepository,
+                Logger,
+                request, item => item is BaseGame,
+                SimilarItemsHelper.GetSimiliarityScore);
+
+            return ToOptimizedResult(result);
+        }
+    }
+}

+ 5 - 204
MediaBrowser.Api/LibraryService.cs

@@ -127,32 +127,6 @@ namespace MediaBrowser.Api
         public Guid? UserId { get; set; }
     }
 
-    [Route("/Items/{Id}/Similar", "GET")]
-    [Api(Description = "Gets items similar to a given input item.")]
-    public class GetSimilarItems : IReturn<ItemsResult>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
-        public Guid? UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        /// <summary>
-        /// The maximum number of items to return
-        /// </summary>
-        /// <value>The limit.</value>
-        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
-        public int? Limit { get; set; }
-    }
-
     /// <summary>
     /// Class LibraryService
     /// </summary>
@@ -420,6 +394,11 @@ namespace MediaBrowser.Api
             Task.WaitAll(task);
         }
 
+        /// <summary>
+        /// Refreshes the item.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>Task.</returns>
         private async Task RefreshItem(RefreshItem request)
         {
             var item = DtoBuilder.GetItemByClientId(request.Id, _userManager, _libraryManager);
@@ -441,183 +420,5 @@ namespace MediaBrowser.Api
                 Logger.ErrorException("Error refreshing library", ex);
             }
         }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetSimilarItems request)
-        {
-            var user = request.UserId.HasValue ? _userManager.GetUserById(request.UserId.Value) : null;
-
-            var item = string.IsNullOrEmpty(request.Id) ?
-                (request.UserId.HasValue ? user.RootFolder :
-                (Folder)_libraryManager.RootFolder) : DtoBuilder.GetItemByClientId(request.Id, _userManager, _libraryManager, request.UserId);
-
-            // Get everything
-            var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)).ToList();
-
-            var dtoBuilder = new DtoBuilder(Logger, _libraryManager, _userDataRepository);
-
-            var inputItems = user == null
-                                 ? _libraryManager.RootFolder.RecursiveChildren
-                                 : user.RootFolder.GetRecursiveChildren(user);
-
-            var items = GetSimilaritems(item, inputItems).ToArray();
-
-            var result = new ItemsResult
-            {
-                Items = items.Take(request.Limit ?? items.Length).Select(i => dtoBuilder.GetBaseItemDto(i, fields, user)).Select(t => t.Result).ToArray(),
-
-                TotalRecordCount = items.Length
-            };
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the similiar items.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="inputItems">The input items.</param>
-        /// <returns>IEnumerable{BaseItem}.</returns>
-        private IEnumerable<BaseItem> GetSimilaritems(BaseItem item, IEnumerable<BaseItem> inputItems)
-        {
-            if (item is Movie || item is Trailer)
-            {
-                inputItems = inputItems.Where(i => i is Movie || i is Trailer);
-            }
-            else if (item is Series)
-            {
-                inputItems = inputItems.Where(i => i is Series);
-            }
-            else if (item is BaseGame)
-            {
-                inputItems = inputItems.Where(i => i is BaseGame);
-            }
-            else if (item is MusicAlbum)
-            {
-                inputItems = inputItems.Where(i => i is MusicAlbum);
-            }
-            else if (item is Audio)
-            {
-                inputItems = inputItems.Where(i => i is Audio);
-            }
-
-            // Avoid implicitly captured closure
-            var currentItem = item;
-
-            return inputItems.Where(i => i.Id != currentItem.Id)
-                .Select(i => new Tuple<BaseItem, int>(i, GetSimiliarityScore(item, i)))
-                .Where(i => i.Item2 > 0)
-                .OrderByDescending(i => i.Item2)
-                .ThenByDescending(i => i.Item1.CriticRating ?? 0)
-                .Select(i => i.Item1);
-        }
-
-        /// <summary>
-        /// Gets the similiarity score.
-        /// </summary>
-        /// <param name="item1">The item1.</param>
-        /// <param name="item2">The item2.</param>
-        /// <returns>System.Int32.</returns>
-        private int GetSimiliarityScore(BaseItem item1, BaseItem item2)
-        {
-            var points = 0;
-
-            if (!string.IsNullOrEmpty(item1.OfficialRating) && string.Equals(item1.OfficialRating, item2.OfficialRating, StringComparison.OrdinalIgnoreCase))
-            {
-                points += 1;
-            }
-
-            // Find common genres
-            points += item1.Genres.Where(i => item2.Genres.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 5);
-
-            // Find common tags
-            points += item1.Tags.Where(i => item2.Tags.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 5);
-
-            // Find common studios
-            points += item1.Studios.Where(i => item2.Studios.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 3);
-
-            var item2PeopleNames = item2.People.Select(i => i.Name).ToList();
-
-            points += item1.People.Where(i => item2PeopleNames.Contains(i.Name, StringComparer.OrdinalIgnoreCase)).Sum(i =>
-            {
-                if (string.Equals(i.Name, PersonType.Director, StringComparison.OrdinalIgnoreCase))
-                {
-                    return 5;
-                }
-                if (string.Equals(i.Name, PersonType.Actor, StringComparison.OrdinalIgnoreCase))
-                {
-                    return 3;
-                }
-                if (string.Equals(i.Name, PersonType.Composer, StringComparison.OrdinalIgnoreCase))
-                {
-                    return 3;
-                }
-                if (string.Equals(i.Name, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
-                {
-                    return 3;
-                }
-                if (string.Equals(i.Name, 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 += 3;
-                }
-
-                // And more if within five years
-                if (diff < 5)
-                {
-                    points += 3;
-                }
-            }
-
-            var album = item1 as MusicAlbum;
-
-            if (album != null)
-            {
-                points += GetAlbumSimilarityScore(album, (MusicAlbum)item2);
-            }
-
-            return points;
-        }
-
-        /// <summary>
-        /// Gets the album similarity score.
-        /// </summary>
-        /// <param name="item1">The item1.</param>
-        /// <param name="item2">The item2.</param>
-        /// <returns>System.Int32.</returns>
-        private int GetAlbumSimilarityScore(MusicAlbum item1, MusicAlbum item2)
-        {
-            var artists1 = item1.RecursiveChildren
-                .OfType<Audio>()
-                .SelectMany(i => new[] { i.AlbumArtist, i.Artist })
-                .Where(i => !string.IsNullOrEmpty(i))
-                .Distinct(StringComparer.OrdinalIgnoreCase)
-                .ToList();
-
-            var artists2 = item2.RecursiveChildren
-                .OfType<Audio>()
-                .SelectMany(i => new[] { i.AlbumArtist, i.Artist })
-                .Where(i => !string.IsNullOrEmpty(i))
-                .Distinct(StringComparer.OrdinalIgnoreCase)
-                .ToList();
-
-            return artists1.Where(i => artists2.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 5);
-        }
     }
 }

+ 5 - 0
MediaBrowser.Api/MediaBrowser.Api.csproj

@@ -64,9 +64,11 @@
     <Compile Include="..\SharedVersion.cs">
       <Link>Properties\SharedVersion.cs</Link>
     </Compile>
+    <Compile Include="AlbumsService.cs" />
     <Compile Include="BaseApiService.cs" />
     <Compile Include="DisplayPreferencesService.cs" />
     <Compile Include="EnvironmentService.cs" />
+    <Compile Include="GamesService.cs" />
     <Compile Include="Images\ImageByNameService.cs" />
     <Compile Include="Images\ImageRequest.cs" />
     <Compile Include="Images\ImageService.cs" />
@@ -76,6 +78,7 @@
     <Compile Include="Library\LibraryService.cs" />
     <Compile Include="Library\LibraryStructureService.cs" />
     <Compile Include="LocalizationService.cs" />
+    <Compile Include="MoviesService.cs" />
     <Compile Include="PackageService.cs" />
     <Compile Include="Playback\Hls\AudioHlsService.cs" />
     <Compile Include="Playback\Hls\BaseHlsService.cs" />
@@ -94,7 +97,9 @@
     <Compile Include="ApiEntryPoint.cs" />
     <Compile Include="SearchService.cs" />
     <Compile Include="SessionsService.cs" />
+    <Compile Include="SimilarItemsHelper.cs" />
     <Compile Include="SystemService.cs" />
+    <Compile Include="TrailersService.cs" />
     <Compile Include="TvShowsService.cs" />
     <Compile Include="UserLibrary\ArtistsService.cs" />
     <Compile Include="UserLibrary\BaseItemsByNameService.cs" />

+ 67 - 0
MediaBrowser.Api/MoviesService.cs

@@ -0,0 +1,67 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using ServiceStack.ServiceHost;
+
+namespace MediaBrowser.Api
+{
+    /// <summary>
+    /// Class GetSimilarMovies
+    /// </summary>
+    [Route("/Movies/{Id}/Similar", "GET")]
+    [Api(Description = "Finds movies and trailers similar to a given movie.")]
+    public class GetSimilarMovies : BaseGetSimilarItems
+    {
+    }
+
+    /// <summary>
+    /// Class MoviesService
+    /// </summary>
+    public class MoviesService : BaseApiService
+    {
+        /// <summary>
+        /// The _user manager
+        /// </summary>
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// The _user data repository
+        /// </summary>
+        private readonly IUserDataRepository _userDataRepository;
+        /// <summary>
+        /// The _library manager
+        /// </summary>
+        private readonly ILibraryManager _libraryManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MoviesService"/> class.
+        /// </summary>
+        /// <param name="userManager">The user manager.</param>
+        /// <param name="userDataRepository">The user data repository.</param>
+        /// <param name="libraryManager">The library manager.</param>
+        public MoviesService(IUserManager userManager, IUserDataRepository userDataRepository, ILibraryManager libraryManager)
+        {
+            _userManager = userManager;
+            _userDataRepository = userDataRepository;
+            _libraryManager = libraryManager;
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetSimilarMovies request)
+        {
+            var result = SimilarItemsHelper.GetSimilarItems(_userManager,
+                _libraryManager,
+                _userDataRepository,
+                Logger,
+                request, item => item is Movie || item is Trailer,
+                SimilarItemsHelper.GetSimiliarityScore);
+
+            return ToOptimizedResult(result);
+        }
+    }
+}

+ 184 - 0
MediaBrowser.Api/SimilarItemsHelper.cs

@@ -0,0 +1,184 @@
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Querying;
+using ServiceStack.ServiceHost;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Api
+{
+    /// <summary>
+    /// Class BaseGetSimilarItems
+    /// </summary>
+    public class BaseGetSimilarItems : IReturn<ItemsResult>
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        [ApiMember(Name = "UserId", Description = "Optional. Filter by user id, and attach user data", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public Guid? UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+
+        /// <summary>
+        /// The maximum number of items to return
+        /// </summary>
+        /// <value>The limit.</value>
+        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? Limit { get; set; }
+    }
+
+    /// <summary>
+    /// Class SimilarItemsHelper
+    /// </summary>
+    public static class SimilarItemsHelper
+    {
+        /// <summary>
+        /// Gets the similar items.
+        /// </summary>
+        /// <param name="userManager">The user manager.</param>
+        /// <param name="libraryManager">The library manager.</param>
+        /// <param name="userDataRepository">The user data repository.</param>
+        /// <param name="logger">The logger.</param>
+        /// <param name="request">The request.</param>
+        /// <param name="includeInSearch">The include in search.</param>
+        /// <param name="getSimilarityScore">The get similarity score.</param>
+        /// <returns>ItemsResult.</returns>
+        internal static ItemsResult GetSimilarItems(IUserManager userManager, ILibraryManager libraryManager, IUserDataRepository userDataRepository, ILogger logger, BaseGetSimilarItems request, Func<BaseItem, bool> includeInSearch, Func<BaseItem, BaseItem, int> getSimilarityScore)
+        {
+            var user = request.UserId.HasValue ? userManager.GetUserById(request.UserId.Value) : null;
+
+            var item = string.IsNullOrEmpty(request.Id) ?
+                (request.UserId.HasValue ? user.RootFolder :
+                (Folder)libraryManager.RootFolder) : DtoBuilder.GetItemByClientId(request.Id, userManager, libraryManager, request.UserId);
+
+            // Get everything
+            var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)).ToList();
+
+            var dtoBuilder = new DtoBuilder(logger, libraryManager, userDataRepository);
+
+            var inputItems = user == null
+                                 ? libraryManager.RootFolder.RecursiveChildren
+                                 : user.RootFolder.GetRecursiveChildren(user);
+
+            var items = GetSimilaritems(item, inputItems, includeInSearch, getSimilarityScore).ToArray();
+
+            var result = new ItemsResult
+            {
+                Items = items.Take(request.Limit ?? items.Length).Select(i => dtoBuilder.GetBaseItemDto(i, fields, user)).Select(t => t.Result).ToArray(),
+
+                TotalRecordCount = items.Length
+            };
+
+            return result;
+        }
+
+        /// <summary>
+        /// Gets the similaritems.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="inputItems">The input items.</param>
+        /// <param name="includeInSearch">The include in search.</param>
+        /// <param name="getSimilarityScore">The get similarity score.</param>
+        /// <returns>IEnumerable{BaseItem}.</returns>
+        private static IEnumerable<BaseItem> GetSimilaritems(BaseItem item, IEnumerable<BaseItem> inputItems, Func<BaseItem, bool> includeInSearch, Func<BaseItem, BaseItem, int> getSimilarityScore)
+        {
+            inputItems = inputItems.Where(includeInSearch);
+
+            // Avoid implicitly captured closure
+            var currentItem = item;
+
+            return inputItems.Where(i => i.Id != currentItem.Id)
+                .Select(i => new Tuple<BaseItem, int>(i, getSimilarityScore(item, i)))
+                .Where(i => i.Item2 > 0)
+                .OrderByDescending(i => i.Item2)
+                .ThenByDescending(i => i.Item1.CriticRating ?? 0)
+                .Select(i => i.Item1);
+        }
+
+        /// <summary>
+        /// Gets the similiarity score.
+        /// </summary>
+        /// <param name="item1">The item1.</param>
+        /// <param name="item2">The item2.</param>
+        /// <returns>System.Int32.</returns>
+        internal static int GetSimiliarityScore(BaseItem item1, BaseItem item2)
+        {
+            var points = 0;
+
+            if (!string.IsNullOrEmpty(item1.OfficialRating) && string.Equals(item1.OfficialRating, item2.OfficialRating, StringComparison.OrdinalIgnoreCase))
+            {
+                points += 1;
+            }
+
+            // Find common genres
+            points += item1.Genres.Where(i => item2.Genres.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 5);
+
+            // Find common tags
+            points += item1.Tags.Where(i => item2.Tags.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 5);
+
+            // Find common studios
+            points += item1.Studios.Where(i => item2.Studios.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 3);
+
+            var item2PeopleNames = item2.People.Select(i => i.Name).ToList();
+
+            points += item1.People.Where(i => item2PeopleNames.Contains(i.Name, StringComparer.OrdinalIgnoreCase)).Sum(i =>
+            {
+                if (string.Equals(i.Name, PersonType.Director, StringComparison.OrdinalIgnoreCase))
+                {
+                    return 5;
+                }
+                if (string.Equals(i.Name, PersonType.Actor, StringComparison.OrdinalIgnoreCase))
+                {
+                    return 3;
+                }
+                if (string.Equals(i.Name, PersonType.Composer, StringComparison.OrdinalIgnoreCase))
+                {
+                    return 3;
+                }
+                if (string.Equals(i.Name, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
+                {
+                    return 3;
+                }
+                if (string.Equals(i.Name, 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 += 3;
+                }
+
+                // And more if within five years
+                if (diff < 5)
+                {
+                    points += 3;
+                }
+            }
+
+            return points;
+        }
+
+    }
+}

+ 67 - 0
MediaBrowser.Api/TrailersService.cs

@@ -0,0 +1,67 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using ServiceStack.ServiceHost;
+
+namespace MediaBrowser.Api
+{
+    /// <summary>
+    /// Class GetSimilarTrailers
+    /// </summary>
+    [Route("/Trailers/{Id}/Similar", "GET")]
+    [Api(Description = "Finds movies and trailers similar to a given trailer.")]
+    public class GetSimilarTrailers : BaseGetSimilarItems
+    {
+    }
+
+    /// <summary>
+    /// Class TrailersService
+    /// </summary>
+    public class TrailersService : BaseApiService
+    {
+        /// <summary>
+        /// The _user manager
+        /// </summary>
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// The _user data repository
+        /// </summary>
+        private readonly IUserDataRepository _userDataRepository;
+        /// <summary>
+        /// The _library manager
+        /// </summary>
+        private readonly ILibraryManager _libraryManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TrailersService"/> class.
+        /// </summary>
+        /// <param name="userManager">The user manager.</param>
+        /// <param name="userDataRepository">The user data repository.</param>
+        /// <param name="libraryManager">The library manager.</param>
+        public TrailersService(IUserManager userManager, IUserDataRepository userDataRepository, ILibraryManager libraryManager)
+        {
+            _userManager = userManager;
+            _userDataRepository = userDataRepository;
+            _libraryManager = libraryManager;
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetSimilarTrailers request)
+        {
+            var result = SimilarItemsHelper.GetSimilarItems(_userManager,
+                _libraryManager,
+                _userDataRepository,
+                Logger,
+                request, item => item is Movie || item is Trailer,
+                SimilarItemsHelper.GetSimiliarityScore);
+
+            return ToOptimizedResult(result);
+        }
+    }
+}

+ 23 - 0
MediaBrowser.Api/TvShowsService.cs

@@ -65,6 +65,12 @@ namespace MediaBrowser.Api
         }
     }
 
+    [Route("/Shows/{Id}/Similar", "GET")]
+    [Api(Description = "Finds tv shows similar to a given one.")]
+    public class GetSimilarShows : BaseGetSimilarItems
+    {
+    }
+    
     /// <summary>
     /// Class TvShowsService
     /// </summary>
@@ -97,6 +103,23 @@ namespace MediaBrowser.Api
             _libraryManager = libraryManager;
         }
 
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetSimilarShows request)
+        {
+            var result = SimilarItemsHelper.GetSimilarItems(_userManager, 
+                _libraryManager, 
+                _userDataRepository, 
+                Logger,
+                request, item => item is Series,
+                SimilarItemsHelper.GetSimiliarityScore);
+
+            return ToOptimizedResult(result);
+        }
+
         /// <summary>
         /// Gets the specified request.
         /// </summary>

+ 41 - 21
MediaBrowser.Server.Implementations/Sqlite/SQLiteDisplayPreferencesRepository.cs

@@ -5,6 +5,7 @@ using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Serialization;
 using System;
 using System.Data;
+using System.Data.SQLite;
 using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
@@ -43,6 +44,8 @@ namespace MediaBrowser.Server.Implementations.Sqlite
         /// </summary>
         private readonly IApplicationPaths _appPaths;
 
+        private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
+        
         /// <summary>
         /// Initializes a new instance of the <see cref="SQLiteUserDataRepository" /> class.
         /// </summary>
@@ -118,34 +121,51 @@ namespace MediaBrowser.Server.Implementations.Sqlite
 
             var serialized = _jsonSerializer.SerializeToBytes(displayPreferences);
 
-            cancellationToken.ThrowIfCancellationRequested();
+            await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            SQLiteTransaction transaction = null;
 
-            using (var cmd = Connection.CreateCommand())
+            try
             {
-                cmd.CommandText = "replace into displaypreferences (id, data) values (@1, @2)";
-                cmd.AddParam("@1", displayPreferences.Id);
-                cmd.AddParam("@2", serialized);
+                transaction = Connection.BeginTransaction();
 
-                using (var tran = Connection.BeginTransaction())
+                using (var cmd = Connection.CreateCommand())
                 {
-                    try
-                    {
-                        cmd.Transaction = tran;
+                    cmd.CommandText = "replace into displaypreferences (id, data) values (@1, @2)";
+                    cmd.AddParam("@1", displayPreferences.Id);
+                    cmd.AddParam("@2", serialized);
 
-                        await cmd.ExecuteNonQueryAsync(cancellationToken);
+                    cmd.Transaction = transaction;
 
-                        tran.Commit();
-                    }
-                    catch (OperationCanceledException)
-                    {
-                        tran.Rollback();
-                    }
-                    catch (Exception e)
-                    {
-                        Logger.ErrorException("Failed to commit transaction.", e);
-                        tran.Rollback();
-                    }
+                    await cmd.ExecuteNonQueryAsync(cancellationToken);
                 }
+
+                transaction.Commit();
+            }
+            catch (OperationCanceledException)
+            {
+                if (transaction != null)
+                {
+                    transaction.Rollback();
+                }
+            }
+            catch (Exception e)
+            {
+                Logger.ErrorException("Failed to save display preferences:", e);
+
+                if (transaction != null)
+                {
+                    transaction.Rollback();
+                }
+            }
+            finally
+            {
+                if (transaction != null)
+                {
+                    transaction.Dispose();
+                }
+
+                _writeLock.Release();
             }
         }
 

+ 1 - 1
MediaBrowser.Server.Implementations/Sqlite/SQLiteItemRepository.cs

@@ -227,7 +227,7 @@ namespace MediaBrowser.Server.Implementations.Sqlite
 
                     await _saveItemCommand.ExecuteNonQueryAsync(cancellationToken);
                 }
-
+                
                 transaction.Commit();
             }
             catch (OperationCanceledException)

+ 44 - 22
MediaBrowser.Server.Implementations/Sqlite/SQLiteUserDataRepository.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Configuration;
+using System.Data.SQLite;
+using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Logging;
@@ -19,6 +20,8 @@ namespace MediaBrowser.Server.Implementations.Sqlite
     {
         private readonly ConcurrentDictionary<string, Task<UserItemData>> _userData = new ConcurrentDictionary<string, Task<UserItemData>>();
 
+        private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
+
         /// <summary>
         /// The repository name
         /// </summary>
@@ -172,34 +175,53 @@ namespace MediaBrowser.Server.Implementations.Sqlite
 
             cancellationToken.ThrowIfCancellationRequested();
 
-            using (var cmd = Connection.CreateCommand())
+            await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            SQLiteTransaction transaction = null;
+
+            try
             {
-                cmd.CommandText = "replace into userdata (key, userId, data) values (@1, @2, @3)";
-                cmd.AddParam("@1", key);
-                cmd.AddParam("@2", userId);
-                cmd.AddParam("@3", serialized);
+                transaction = Connection.BeginTransaction();
 
-                using (var tran = Connection.BeginTransaction())
+                using (var cmd = Connection.CreateCommand())
                 {
-                    try
-                    {
-                        cmd.Transaction = tran;
+                    cmd.CommandText = "replace into userdata (key, userId, data) values (@1, @2, @3)";
+                    cmd.AddParam("@1", key);
+                    cmd.AddParam("@2", userId);
+                    cmd.AddParam("@3", serialized);
 
-                        await cmd.ExecuteNonQueryAsync(cancellationToken);
+                    cmd.Transaction = transaction;
 
-                        tran.Commit();
-                    }
-                    catch (OperationCanceledException)
-                    {
-                        tran.Rollback();
-                    }
-                    catch (Exception e)
-                    {
-                        Logger.ErrorException("Failed to commit transaction.", e);
-                        tran.Rollback();
-                    }
+                    await cmd.ExecuteNonQueryAsync(cancellationToken);
+                }
+
+                transaction.Commit();
+            }
+            catch (OperationCanceledException)
+            {
+                if (transaction != null)
+                {
+                    transaction.Rollback();
                 }
             }
+            catch (Exception e)
+            {
+                Logger.ErrorException("Failed to save user data:", e);
+
+                if (transaction != null)
+                {
+                    transaction.Rollback();
+                }
+            }
+            finally
+            {
+                if (transaction != null)
+                {
+                    transaction.Dispose();
+                }
+
+                _writeLock.Release();
+            }
         }
 
         /// <summary>

+ 46 - 2
MediaBrowser.WebDashboard/ApiClient.js

@@ -257,9 +257,53 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
             });
         };
 
-        self.getSimilarItems = function (itemId, options) {
+        self.getSimilarMovies = function (itemId, options) {
 
-            var url = self.getUrl("Items/" + itemId + "/Similar", options);
+            var url = self.getUrl("Movies/" + itemId + "/Similar", options);
+
+            return self.ajax({
+                type: "GET",
+                url: url,
+                dataType: "json"
+            });
+        };
+
+        self.getSimilarTrailers = function (itemId, options) {
+
+            var url = self.getUrl("Trailers/" + itemId + "/Similar", options);
+
+            return self.ajax({
+                type: "GET",
+                url: url,
+                dataType: "json"
+            });
+        };
+
+        self.getSimilarShows = function (itemId, options) {
+
+            var url = self.getUrl("Shows/" + itemId + "/Similar", options);
+
+            return self.ajax({
+                type: "GET",
+                url: url,
+                dataType: "json"
+            });
+        };
+
+        self.getSimilarAlbums = function (itemId, options) {
+
+            var url = self.getUrl("Albums/" + itemId + "/Similar", options);
+
+            return self.ajax({
+                type: "GET",
+                url: url,
+                dataType: "json"
+            });
+        };
+
+        self.getSimilarGames = function (itemId, options) {
+
+            var url = self.getUrl("Games/" + itemId + "/Similar", options);
 
             return self.ajax({
                 type: "GET",

+ 1 - 1
MediaBrowser.WebDashboard/packages.config

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
-  <package id="MediaBrowser.ApiClient.Javascript" version="3.0.118" targetFramework="net45" />
+  <package id="MediaBrowser.ApiClient.Javascript" version="3.0.119" targetFramework="net45" />
   <package id="ServiceStack.Common" version="3.9.46" targetFramework="net45" />
   <package id="ServiceStack.Text" version="3.9.45" targetFramework="net45" />
 </packages>