Luke Pulverenti пре 12 година
родитељ
комит
374b7f2f03
51 измењених фајлова са 1556 додато и 559 уклоњено
  1. 25 0
      MediaBrowser.Api/Images/ImageService.cs
  2. 1 136
      MediaBrowser.Api/Library/LibraryService.cs
  3. 1 0
      MediaBrowser.Api/MediaBrowser.Api.csproj
  4. 187 0
      MediaBrowser.Api/UserLibrary/ArtistsService.cs
  5. 55 11
      MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
  6. 0 7
      MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
  7. 81 29
      MediaBrowser.Api/UserLibrary/GenresService.cs
  8. 111 55
      MediaBrowser.Api/UserLibrary/ItemByNameUserDataService.cs
  9. 8 1
      MediaBrowser.Api/UserLibrary/ItemsService.cs
  10. 65 9
      MediaBrowser.Api/UserLibrary/PersonsService.cs
  11. 66 10
      MediaBrowser.Api/UserLibrary/StudiosService.cs
  12. 65 5
      MediaBrowser.Api/UserLibrary/YearsService.cs
  13. 1 1
      MediaBrowser.Controller/Dto/DtoBuilder.cs
  14. 1 1
      MediaBrowser.Controller/Entities/Audio/Artist.cs
  15. 10 0
      MediaBrowser.Controller/Entities/Audio/Audio.cs
  16. 1 1
      MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
  17. 0 8
      MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
  18. 9 7
      MediaBrowser.Controller/Entities/BaseItem.cs
  19. 1 1
      MediaBrowser.Controller/Entities/Genre.cs
  20. 1 1
      MediaBrowser.Controller/Entities/Person.cs
  21. 1 1
      MediaBrowser.Controller/Entities/Studio.cs
  22. 1 1
      MediaBrowser.Controller/Entities/Year.cs
  23. 8 0
      MediaBrowser.Controller/Library/ILibraryManager.cs
  24. 9 0
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  25. 2 1
      MediaBrowser.Controller/Providers/IProviderManager.cs
  26. 5 5
      MediaBrowser.Controller/Providers/Movies/FanArtMovieProvider.cs
  27. 4 7
      MediaBrowser.Controller/Providers/Movies/MovieDbProvider.cs
  28. 1 1
      MediaBrowser.Controller/Providers/Movies/TmdbPersonProvider.cs
  29. 73 33
      MediaBrowser.Controller/Providers/Music/FanArtAlbumProvider.cs
  30. 48 0
      MediaBrowser.Controller/Providers/Music/FanArtArtistByNameProvider.cs
  31. 25 51
      MediaBrowser.Controller/Providers/Music/FanArtArtistProvider.cs
  32. 59 26
      MediaBrowser.Controller/Providers/Music/LastfmAlbumProvider.cs
  33. 50 0
      MediaBrowser.Controller/Providers/Music/LastfmArtistByNameProvider.cs
  34. 5 16
      MediaBrowser.Controller/Providers/Music/LastfmArtistProvider.cs
  35. 19 60
      MediaBrowser.Controller/Providers/Music/LastfmBaseProvider.cs
  36. 13 7
      MediaBrowser.Controller/Providers/Music/LastfmHelper.cs
  37. 3 3
      MediaBrowser.Controller/Providers/TV/FanArtTVProvider.cs
  38. 1 1
      MediaBrowser.Controller/Providers/TV/RemoteEpisodeProvider.cs
  39. 4 4
      MediaBrowser.Controller/Providers/TV/RemoteSeasonProvider.cs
  40. 4 4
      MediaBrowser.Controller/Providers/TV/RemoteSeriesProvider.cs
  41. 4 0
      MediaBrowser.Controller/packages.config
  42. 102 6
      MediaBrowser.Server.Implementations/Library/LibraryManager.cs
  43. 1 0
      MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
  44. 4 3
      MediaBrowser.Server.Implementations/Providers/ProviderManager.cs
  45. 81 0
      MediaBrowser.Server.Implementations/ScheduledTasks/ArtistValidationTask.cs
  46. 1 1
      MediaBrowser.Server.Implementations/ScheduledTasks/PeopleValidationTask.cs
  47. 3 1
      MediaBrowser.Server.Implementations/Sqlite/SQLiteRepository.cs
  48. 2 0
      MediaBrowser.WebDashboard/Api/DashboardService.cs
  49. 321 43
      MediaBrowser.WebDashboard/ApiClient.js
  50. 12 0
      MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
  51. 1 1
      MediaBrowser.WebDashboard/packages.config

+ 25 - 0
MediaBrowser.Api/Images/ImageService.cs

@@ -48,6 +48,19 @@ namespace MediaBrowser.Api.Images
         public string Name { get; set; }
     }
 
+    [Route("/Artists/{Name}/Images/{Type}", "GET")]
+    [Route("/Artists/{Name}/Images/{Type}/{Index}", "GET")]
+    [Api(Description = "Gets an artist image")]
+    public class GetArtistImage : ImageRequest
+    {
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        [ApiMember(Name = "Name", Description = "Artist name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Name { get; set; }
+    }
+
     /// <summary>
     /// Class GetStudioImage
     /// </summary>
@@ -233,6 +246,18 @@ namespace MediaBrowser.Api.Images
             return GetImage(request, item);
         }
 
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetArtistImage request)
+        {
+            var item = _libraryManager.GetArtist(request.Name).Result;
+
+            return GetImage(request, item);
+        }
+        
         /// <summary>
         /// Gets the specified request.
         /// </summary>

+ 1 - 136
MediaBrowser.Api/Library/LibraryService.cs

@@ -1,10 +1,6 @@
 using MediaBrowser.Common;
-using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Querying;
 using ServiceStack.ServiceHost;
 using System;
 using System.Collections.Generic;
@@ -36,66 +32,6 @@ namespace MediaBrowser.Api.Library
         public bool HasInternetProvider { get; set; }
     }
 
-    /// <summary>
-    /// Class GetPerson
-    /// </summary>
-    [Route("/Persons/{Name}", "GET")]
-    [Api(Description = "Gets a person, by name")]
-    public class GetPerson : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The person name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetStudio
-    /// </summary>
-    [Route("/Studios/{Name}", "GET")]
-    [Api(Description = "Gets a studio, by name")]
-    public class GetStudio : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The studio name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetGenre
-    /// </summary>
-    [Route("/Genres/{Name}", "GET")]
-    [Api(Description = "Gets a genre, by name")]
-    public class GetGenre : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The genre name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetYear
-    /// </summary>
-    [Route("/Years/{Year}", "GET")]
-    [Api(Description = "Gets a year")]
-    public class GetYear : IReturn<BaseItemDto>
-    {
-        /// <summary>
-        /// Gets or sets the year.
-        /// </summary>
-        /// <value>The year.</value>
-        [ApiMember(Name = "Year", Description = "The year", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
-        public int Year { get; set; }
-    }
-
     /// <summary>
     /// Class LibraryService
     /// </summary>
@@ -106,16 +42,14 @@ namespace MediaBrowser.Api.Library
         /// </summary>
         private readonly IApplicationHost _appHost;
         private readonly ILibraryManager _libraryManager;
-        private readonly IUserDataRepository _userDataRepository;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="LibraryService" /> class.
         /// </summary>
         /// <param name="appHost">The app host.</param>
         /// <param name="libraryManager">The library manager.</param>
-        /// <param name="userDataRepository">The user data repository.</param>
         /// <exception cref="System.ArgumentNullException">appHost</exception>
-        public LibraryService(IApplicationHost appHost, ILibraryManager libraryManager, IUserDataRepository userDataRepository)
+        public LibraryService(IApplicationHost appHost, ILibraryManager libraryManager)
         {
             if (appHost == null)
             {
@@ -124,75 +58,6 @@ namespace MediaBrowser.Api.Library
 
             _appHost = appHost;
             _libraryManager = libraryManager;
-            _userDataRepository = userDataRepository;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetPerson request)
-        {
-            var item = _libraryManager.GetPerson(request.Name).Result;
-
-            // Get everything
-            var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true));
-
-            var result = new DtoBuilder(Logger, _libraryManager, _userDataRepository).GetBaseItemDto(item, fields.ToList()).Result;
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetGenre request)
-        {
-            var item = _libraryManager.GetGenre(request.Name).Result;
-
-            // Get everything
-            var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true));
-
-            var result = new DtoBuilder(Logger, _libraryManager, _userDataRepository).GetBaseItemDto(item, fields.ToList()).Result;
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetStudio request)
-        {
-            var item = _libraryManager.GetStudio(request.Name).Result;
-
-            // Get everything
-            var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true));
-
-            var result = new DtoBuilder(Logger, _libraryManager, _userDataRepository).GetBaseItemDto(item, fields.ToList()).Result;
-
-            return ToOptimizedResult(result);
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetYear request)
-        {
-            var item = _libraryManager.GetYear(request.Year).Result;
-
-            // Get everything
-            var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true));
-
-            var result = new DtoBuilder(Logger, _libraryManager, _userDataRepository).GetBaseItemDto(item, fields.ToList()).Result;
-
-            return ToOptimizedResult(result);
         }
 
         /// <summary>

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

@@ -87,6 +87,7 @@
     <Compile Include="ScheduledTasks\ScheduledTasksWebSocketListener.cs" />
     <Compile Include="ApiEntryPoint.cs" />
     <Compile Include="SystemService.cs" />
+    <Compile Include="UserLibrary\ArtistsService.cs" />
     <Compile Include="UserLibrary\BaseItemsByNameService.cs" />
     <Compile Include="UserLibrary\BaseItemsRequest.cs" />
     <Compile Include="UserLibrary\GenresService.cs" />

+ 187 - 0
MediaBrowser.Api/UserLibrary/ArtistsService.cs

@@ -0,0 +1,187 @@
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using ServiceStack.ServiceHost;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.UserLibrary
+{
+    /// <summary>
+    /// Class GetArtists
+    /// </summary>
+    [Route("/Artists", "GET")]
+    [Api(Description = "Gets all artists from a given item, folder, or the entire library")]
+    public class GetArtists : GetItemsByName
+    {
+    }
+
+    /// <summary>
+    /// Class GetArtistsItemCounts
+    /// </summary>
+    [Route("/Artists/{Name}/Counts", "GET")]
+    [Api(Description = "Gets item counts of library items that an artist appears in")]
+    public class GetArtistsItemCounts : IReturn<ItemByNameCounts>
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public Guid UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        [ApiMember(Name = "Name", Description = "The artist name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Name { get; set; }
+    }
+
+    [Route("/Artists/{Name}", "GET")]
+    [Api(Description = "Gets an artist, by name")]
+    public class GetArtist : IReturn<BaseItemDto>
+    {
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        [ApiMember(Name = "Name", Description = "The artist name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Name { get; set; }
+
+        /// <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>
+    /// Class ArtistsService
+    /// </summary>
+    public class ArtistsService : BaseItemsByNameService<Artist>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ArtistsService"/> class.
+        /// </summary>
+        /// <param name="userManager">The user manager.</param>
+        /// <param name="libraryManager">The library manager.</param>
+        /// <param name="userDataRepository">The user data repository.</param>
+        public ArtistsService(IUserManager userManager, ILibraryManager libraryManager, IUserDataRepository userDataRepository)
+            : base(userManager, libraryManager, userDataRepository)
+        {
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetArtist request)
+        {
+            var result = GetItem(request).Result;
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the item.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>Task{BaseItemDto}.</returns>
+        private async Task<BaseItemDto> GetItem(GetArtist request)
+        {
+            var item = await LibraryManager.GetArtist(request.Name).ConfigureAwait(false);
+
+            // Get everything
+            var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true));
+
+            var builder = new DtoBuilder(Logger, LibraryManager, UserDataRepository);
+
+            if (request.UserId.HasValue)
+            {
+                var user = UserManager.GetUserById(request.UserId.Value);
+
+                return await builder.GetBaseItemDto(item, user, fields.ToList()).ConfigureAwait(false);
+            }
+
+            return await builder.GetBaseItemDto(item, fields.ToList()).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetArtistsItemCounts request)
+        {
+            var items = GetItems(request.UserId).OfType<Audio>().Where(i => i.HasArtist(request.Name)).ToList();
+
+            var counts = new ItemByNameCounts
+            {
+                TotalCount = items.Count,
+
+                SongCount = items.Count(),
+
+                AlbumCount = items.Select(i => i.Parent).OfType<MusicAlbum>().Distinct().Count()
+            };
+
+            return ToOptimizedResult(counts);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetArtists request)
+        {
+            var result = GetResult(request).Result;
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets all items.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <param name="items">The items.</param>
+        /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
+        protected override IEnumerable<IbnStub<Artist>> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items)
+        {
+            var itemsList = items.OfType<Audio>().ToList();
+
+            return itemsList
+                .SelectMany(i =>
+                    {
+                        var list = i.Artists.ToList();
+
+                        if (!string.IsNullOrEmpty(i.AlbumArtist))
+                        {
+                            list.Add(i.AlbumArtist);
+                        }
+
+                        return list;
+                    })
+                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .Select(name => new IbnStub<Artist>(name, () => itemsList.Where(i => i.HasArtist(name)), GetEntity));
+        }
+
+        /// <summary>
+        /// Gets the entity.
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <returns>Task{Artist}.</returns>
+        protected Task<Artist> GetEntity(string name)
+        {
+            return LibraryManager.GetArtist(name);
+        }
+    }
+}

+ 55 - 11
MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs

@@ -50,9 +50,18 @@ namespace MediaBrowser.Api.UserLibrary
         /// <returns>Task{ItemsResult}.</returns>
         protected async Task<ItemsResult> GetResult(GetItemsByName request)
         {
-            var user = UserManager.GetUserById(request.UserId);
+            User user = null;
+            BaseItem item;
 
-            var item = string.IsNullOrEmpty(request.ParentId) ? user.RootFolder : DtoBuilder.GetItemByClientId(request.ParentId, UserManager, LibraryManager, user.Id);
+            if (request.UserId.HasValue)
+            {
+                user = UserManager.GetUserById(request.UserId.Value);
+                item = string.IsNullOrEmpty(request.ParentId) ? user.RootFolder : DtoBuilder.GetItemByClientId(request.ParentId, UserManager, LibraryManager, user.Id);
+            }
+            else
+            {
+                item = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : DtoBuilder.GetItemByClientId(request.ParentId, UserManager, LibraryManager);
+            }
 
             IEnumerable<BaseItem> items;
 
@@ -60,16 +69,23 @@ namespace MediaBrowser.Api.UserLibrary
             {
                 var folder = (Folder)item;
 
-                items = request.Recursive ? folder.GetRecursiveChildren(user) : folder.GetChildren(user);
+                if (request.UserId.HasValue)
+                {
+                    items = request.Recursive ? folder.GetRecursiveChildren(user) : folder.GetChildren(user);
+                }
+                else
+                {
+                    items = request.Recursive ? folder.RecursiveChildren: folder.Children;
+                }
             }
             else
             {
                 items = new[] { item };
             }
 
-            items = FilterItems(request, items, user);
+            items = FilterItems(request, items);
 
-            var extractedItems = GetAllItems(request, items, user);
+            var extractedItems = GetAllItems(request, items);
 
             extractedItems = FilterItems(request, extractedItems, user);
             extractedItems = SortItems(request, extractedItems);
@@ -187,9 +203,8 @@ namespace MediaBrowser.Api.UserLibrary
         /// </summary>
         /// <param name="request">The request.</param>
         /// <param name="items">The items.</param>
-        /// <param name="user">The user.</param>
         /// <returns>IEnumerable{BaseItem}.</returns>
-        private IEnumerable<BaseItem> FilterItems(GetItemsByName request, IEnumerable<BaseItem> items, User user)
+        private IEnumerable<BaseItem> FilterItems(GetItemsByName request, IEnumerable<BaseItem> items)
         {
             // Exclude item types
             if (!string.IsNullOrEmpty(request.ExcludeItemTypes))
@@ -213,9 +228,8 @@ namespace MediaBrowser.Api.UserLibrary
         /// </summary>
         /// <param name="request">The request.</param>
         /// <param name="items">The items.</param>
-        /// <param name="user">The user.</param>
         /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
-        protected abstract IEnumerable<IbnStub<TItemType>> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items, User user);
+        protected abstract IEnumerable<IbnStub<TItemType>> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items);
 
         /// <summary>
         /// Gets the dto.
@@ -238,18 +252,36 @@ namespace MediaBrowser.Api.UserLibrary
                 return null;
             }
 
-            var dto = await new DtoBuilder(Logger, LibraryManager, UserDataRepository).GetBaseItemDto(item, user, fields).ConfigureAwait(false);
+            var dto = user == null ? await new DtoBuilder(Logger, LibraryManager, UserDataRepository).GetBaseItemDto(item, fields).ConfigureAwait(false) :
+                await new DtoBuilder(Logger, LibraryManager, UserDataRepository).GetBaseItemDto(item, user, fields).ConfigureAwait(false);
 
             if (fields.Contains(ItemFields.ItemCounts))
             {
                 var items = stub.Items;
 
                 dto.ChildCount = items.Count;
-                dto.RecentlyAddedItemCount = items.Count(i => i.IsRecentlyAdded(user));
+                dto.RecentlyAddedItemCount = items.Count(i => i.IsRecentlyAdded());
             }
 
             return dto;
         }
+
+        /// <summary>
+        /// Gets the items.
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <returns>IEnumerable{BaseItem}.</returns>
+        protected IEnumerable<BaseItem> GetItems(Guid? userId)
+        {
+            if (userId.HasValue)
+            {
+                var user = UserManager.GetUserById(userId.Value);
+
+                return UserManager.GetUserById(userId.Value).RootFolder.GetRecursiveChildren(user);
+            }
+
+            return LibraryManager.RootFolder.RecursiveChildren;
+        }
     }
 
     /// <summary>
@@ -257,12 +289,24 @@ namespace MediaBrowser.Api.UserLibrary
     /// </summary>
     public class GetItemsByName : BaseItemsRequest, 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>
         /// What to sort the results by
         /// </summary>
         /// <value>The sort by.</value>
         [ApiMember(Name = "SortBy", Description = "Optional. Options: SortName", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
         public string SortBy { get; set; }
+
+        public GetItemsByName()
+        {
+            Recursive = true;
+        }
     }
 
     public class IbnStub<T>

+ 0 - 7
MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs

@@ -9,13 +9,6 @@ namespace MediaBrowser.Api.UserLibrary
 {
     public abstract class BaseItemsRequest
     {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
-
         /// <summary>
         /// Skips over a given number of items within the results. Use for paging.
         /// </summary>

+ 81 - 29
MediaBrowser.Api/UserLibrary/GenresService.cs

@@ -1,11 +1,12 @@
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
 using ServiceStack.ServiceHost;
 using System;
 using System.Collections.Generic;
@@ -17,17 +18,13 @@ namespace MediaBrowser.Api.UserLibrary
     /// <summary>
     /// Class GetGenres
     /// </summary>
-    [Route("/Users/{UserId}/Items/{ParentId}/Genres", "GET")]
-    [Route("/Users/{UserId}/Items/Root/Genres", "GET")]
+    [Route("/Genres", "GET")]
     [Api(Description = "Gets all genres from a given item, folder, or the entire library")]
     public class GetGenres : GetItemsByName
     {
     }
 
-    /// <summary>
-    /// Class GetGenreItemCounts
-    /// </summary>
-    [Route("/Users/{UserId}/Genres/{Name}/Counts", "GET")]
+    [Route("/Genres/{Name}/Counts", "GET")]
     [Api(Description = "Gets item counts of library items that a genre appears in")]
     public class GetGenreItemCounts : IReturn<ItemByNameCounts>
     {
@@ -35,8 +32,8 @@ namespace MediaBrowser.Api.UserLibrary
         /// Gets or sets the user id.
         /// </summary>
         /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public Guid? UserId { get; set; }
 
         /// <summary>
         /// Gets or sets the name.
@@ -45,6 +42,28 @@ namespace MediaBrowser.Api.UserLibrary
         [ApiMember(Name = "Name", Description = "The genre name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
         public string Name { get; set; }
     }
+
+    /// <summary>
+    /// Class GetGenre
+    /// </summary>
+    [Route("/Genres/{Name}", "GET")]
+    [Api(Description = "Gets a genre, by name")]
+    public class GetGenre : IReturn<BaseItemDto>
+    {
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        [ApiMember(Name = "Name", Description = "The genre name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Name { get; set; }
+
+        /// <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>
     /// Class GenresService
@@ -61,32 +80,37 @@ namespace MediaBrowser.Api.UserLibrary
         /// </summary>
         /// <param name="request">The request.</param>
         /// <returns>System.Object.</returns>
-        public object Get(GetGenreItemCounts request)
+        public object Get(GetGenre request)
         {
-            var user = UserManager.GetUserById(request.UserId);
-
-            var items = user.RootFolder.GetRecursiveChildren(user).Where(i => i.Genres != null && i.Genres.Contains(request.Name, StringComparer.OrdinalIgnoreCase)).ToList();
+            var result = GetItem(request).Result;
 
-            var counts = new ItemByNameCounts
-            {
-                TotalCount = items.Count,
-
-                TrailerCount = items.OfType<Trailer>().Count(),
+            return ToOptimizedResult(result);
+        }
 
-                MovieCount = items.OfType<Movie>().Count(),
+        /// <summary>
+        /// Gets the item.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>Task{BaseItemDto}.</returns>
+        private async Task<BaseItemDto> GetItem(GetGenre request)
+        {
+            var item = await LibraryManager.GetGenre(request.Name).ConfigureAwait(false);
 
-                SeriesCount = items.OfType<Series>().Count(),
+            // Get everything
+            var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true));
 
-                GameCount = items.OfType<BaseGame>().Count(),
+            var builder = new DtoBuilder(Logger, LibraryManager, UserDataRepository);
 
-                SongCount = items.OfType<Audio>().Count(),
+            if (request.UserId.HasValue)
+            {
+                var user = UserManager.GetUserById(request.UserId.Value);
 
-                AlbumCount = items.OfType<MusicAlbum>().Count()
-            };
+                return await builder.GetBaseItemDto(item, user, fields.ToList()).ConfigureAwait(false);
+            }
 
-            return ToOptimizedResult(counts);
+            return await builder.GetBaseItemDto(item, fields.ToList()).ConfigureAwait(false);
         }
-        
+       
         /// <summary>
         /// Gets the specified request.
         /// </summary>
@@ -104,9 +128,8 @@ namespace MediaBrowser.Api.UserLibrary
         /// </summary>
         /// <param name="request">The request.</param>
         /// <param name="items">The items.</param>
-        /// <param name="user">The user.</param>
         /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
-        protected override IEnumerable<IbnStub<Genre>> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items, User user)
+        protected override IEnumerable<IbnStub<Genre>> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items)
         {
             var itemsList = items.Where(i => i.Genres != null).ToList();
 
@@ -125,5 +148,34 @@ namespace MediaBrowser.Api.UserLibrary
         {
             return LibraryManager.GetGenre(name);
         }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetGenreItemCounts request)
+        {
+            var items = GetItems(request.UserId).Where(i => i.Genres != null && i.Genres.Contains(request.Name, StringComparer.OrdinalIgnoreCase)).ToList();
+
+            var counts = new ItemByNameCounts
+            {
+                TotalCount = items.Count,
+
+                TrailerCount = items.OfType<Trailer>().Count(),
+
+                MovieCount = items.OfType<Movie>().Count(),
+
+                SeriesCount = items.OfType<Series>().Count(),
+
+                GameCount = items.OfType<BaseGame>().Count(),
+
+                SongCount = items.OfType<Audio>().Count(),
+
+                AlbumCount = items.OfType<MusicAlbum>().Count()
+            };
+
+            return ToOptimizedResult(counts);
+        }
     }
 }

+ 111 - 55
MediaBrowser.Api/UserLibrary/ItemByNameUserDataService.cs

@@ -1,38 +1,21 @@
-using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using ServiceStack.ServiceHost;
+using ServiceStack.Text.Controller;
 using System;
 using System.Threading;
 using System.Threading.Tasks;
 
 namespace MediaBrowser.Api.UserLibrary
 {
-    /// <summary>
-    /// Class GetItemByNameUserData
-    /// </summary>
-    [Route("/Users/{UserId}/ItemsByName/{Name}/UserData", "GET")]
-    [Api(Description = "Gets user data for an item")]
-    public class GetItemByNameUserData : IReturnVoid
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public Guid UserId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The item name (genre, person, year, studio, artist, album)", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-    }
-
     /// <summary>
     /// Class MarkItemByNameFavorite
     /// </summary>
-    [Route("/Users/{UserId}/ItemsByName/Favorites/{Name}", "POST")]
+    [Route("/Users/{UserId}/Favorites/Artists/{Name}", "POST")]
+    [Route("/Users/{UserId}/Favorites/Persons/{Name}", "POST")]
+    [Route("/Users/{UserId}/Favorites/Studios/{Name}", "POST")]
+    [Route("/Users/{UserId}/Favorites/Genres/{Name}", "POST")]
     [Api(Description = "Marks something as a favorite")]
     public class MarkItemByNameFavorite : IReturnVoid
     {
@@ -47,14 +30,17 @@ namespace MediaBrowser.Api.UserLibrary
         /// Gets or sets the name.
         /// </summary>
         /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The item name (genre, person, year, studio, artist, album)", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        [ApiMember(Name = "Name", Description = "The name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
         public string Name { get; set; }
     }
 
     /// <summary>
     /// Class UnmarkItemByNameFavorite
     /// </summary>
-    [Route("/Users/{UserId}/ItemsByName/Favorites/{Name}", "DELETE")]
+    [Route("/Users/{UserId}/Favorites/Artists/{Name}", "DELETE")]
+    [Route("/Users/{UserId}/Favorites/Persons/{Name}", "DELETE")]
+    [Route("/Users/{UserId}/Favorites/Studios/{Name}", "DELETE")]
+    [Route("/Users/{UserId}/Favorites/Genres/{Name}", "DELETE")]
     [Api(Description = "Unmarks something as a favorite")]
     public class UnmarkItemByNameFavorite : IReturnVoid
     {
@@ -69,11 +55,17 @@ namespace MediaBrowser.Api.UserLibrary
         /// Gets or sets the name.
         /// </summary>
         /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The item name (genre, person, year, studio, artist, album)", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
+        [ApiMember(Name = "Name", Description = "The name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
         public string Name { get; set; }
     }
 
-    [Route("/Users/{UserId}/ItemsByName/{Name}/Rating", "POST")]
+    /// <summary>
+    /// Class UpdateItemByNameRating
+    /// </summary>
+    [Route("/Users/{UserId}/Ratings/Artists/{Name}", "POST")]
+    [Route("/Users/{UserId}/Ratings/Persons/{Name}", "POST")]
+    [Route("/Users/{UserId}/Ratings/Studios/{Name}", "POST")]
+    [Route("/Users/{UserId}/Ratings/Genres/{Name}", "POST")]
     [Api(Description = "Updates a user's rating for an item")]
     public class UpdateItemByNameRating : IReturnVoid
     {
@@ -88,7 +80,7 @@ namespace MediaBrowser.Api.UserLibrary
         /// Gets or sets the name.
         /// </summary>
         /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The item name (genre, person, year, studio, artist, album)", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        [ApiMember(Name = "Name", Description = "The name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
         public string Name { get; set; }
 
         /// <summary>
@@ -99,7 +91,13 @@ namespace MediaBrowser.Api.UserLibrary
         public bool Likes { get; set; }
     }
 
-    [Route("/Users/{UserId}/ItemsByName/{Name}/Rating", "DELETE")]
+    /// <summary>
+    /// Class DeleteItemByNameRating
+    /// </summary>
+    [Route("/Users/{UserId}/Ratings/Artists/{Name}", "DELETE")]
+    [Route("/Users/{UserId}/Ratings/Persons/{Name}", "DELETE")]
+    [Route("/Users/{UserId}/Ratings/Studios/{Name}", "DELETE")]
+    [Route("/Users/{UserId}/Ratings/Genres/{Name}", "DELETE")]
     [Api(Description = "Deletes a user's saved personal rating for an item")]
     public class DeleteItemByNameRating : IReturnVoid
     {
@@ -114,10 +112,10 @@ namespace MediaBrowser.Api.UserLibrary
         /// Gets or sets the name.
         /// </summary>
         /// <value>The name.</value>
-        [ApiMember(Name = "Name", Description = "The item name (genre, person, year, studio, artist, album)", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
+        [ApiMember(Name = "Name", Description = "The item name (genre, person, year, studio, artist)", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
         public string Name { get; set; }
     }
-    
+
     /// <summary>
     /// Class ItemByNameUserDataService
     /// </summary>
@@ -128,35 +126,32 @@ namespace MediaBrowser.Api.UserLibrary
         /// </summary>
         protected readonly IUserDataRepository UserDataRepository;
 
+        /// <summary>
+        /// The library manager
+        /// </summary>
+        protected readonly ILibraryManager LibraryManager;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ItemByNameUserDataService" /> class.
         /// </summary>
         /// <param name="userDataRepository">The user data repository.</param>
-        public ItemByNameUserDataService(IUserDataRepository userDataRepository)
+        /// <param name="libraryManager">The library manager.</param>
+        public ItemByNameUserDataService(IUserDataRepository userDataRepository, ILibraryManager libraryManager)
         {
             UserDataRepository = userDataRepository;
+            LibraryManager = libraryManager;
         }
 
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetItemByNameUserData request)
-        {
-            // Get the user data for this item
-            var data = UserDataRepository.GetUserData(request.UserId, request.Name).Result;
-
-            return ToOptimizedResult(DtoBuilder.GetUserItemDataDto(data));
-        }
-        
         /// <summary>
         /// Posts the specified request.
         /// </summary>
         /// <param name="request">The request.</param>
         public void Post(MarkItemByNameFavorite request)
         {
-            var task = MarkFavorite(request.UserId, request.Name, true);
+            var pathInfo = PathInfo.Parse(RequestContext.PathInfo);
+            var type = pathInfo.GetArgumentValue<string>(3);
+
+            var task = MarkFavorite(request.UserId, type, request.Name, true);
 
             Task.WaitAll(task);
         }
@@ -167,18 +162,24 @@ namespace MediaBrowser.Api.UserLibrary
         /// <param name="request">The request.</param>
         public void Post(UpdateItemByNameRating request)
         {
-            var task = MarkLike(request.UserId, request.Name, request.Likes);
+            var pathInfo = PathInfo.Parse(RequestContext.PathInfo);
+            var type = pathInfo.GetArgumentValue<string>(3);
+
+            var task = MarkLike(request.UserId, type, request.Name, request.Likes);
 
             Task.WaitAll(task);
         }
-        
+
         /// <summary>
         /// Deletes the specified request.
         /// </summary>
         /// <param name="request">The request.</param>
         public void Delete(UnmarkItemByNameFavorite request)
         {
-            var task = MarkFavorite(request.UserId, request.Name, false);
+            var pathInfo = PathInfo.Parse(RequestContext.PathInfo);
+            var type = pathInfo.GetArgumentValue<string>(3);
+
+            var task = MarkFavorite(request.UserId, type, request.Name, false);
 
             Task.WaitAll(task);
         }
@@ -189,7 +190,10 @@ namespace MediaBrowser.Api.UserLibrary
         /// <param name="request">The request.</param>
         public void Delete(DeleteItemByNameRating request)
         {
-            var task = MarkLike(request.UserId, request.Name, null);
+            var pathInfo = PathInfo.Parse(RequestContext.PathInfo);
+            var type = pathInfo.GetArgumentValue<string>(3);
+
+            var task = MarkLike(request.UserId, type, request.Name, null);
 
             Task.WaitAll(task);
         }
@@ -198,11 +202,37 @@ namespace MediaBrowser.Api.UserLibrary
         /// Marks the favorite.
         /// </summary>
         /// <param name="userId">The user id.</param>
-        /// <param name="key">The key.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="name">The name.</param>
         /// <param name="isFavorite">if set to <c>true</c> [is favorite].</param>
         /// <returns>Task.</returns>
-        protected async Task MarkFavorite(Guid userId, string key, bool isFavorite)
+        protected async Task MarkFavorite(Guid userId, string type, string name, bool isFavorite)
         {
+            BaseItem item;
+
+            if (string.Equals(type, "Persons"))
+            {
+                item = await LibraryManager.GetPerson(name).ConfigureAwait(false);
+            }
+            else if (string.Equals(type, "Artists"))
+            {
+                item = await LibraryManager.GetArtist(name).ConfigureAwait(false);
+            }
+            else if (string.Equals(type, "Genres"))
+            {
+                item = await LibraryManager.GetGenre(name).ConfigureAwait(false);
+            }
+            else if (string.Equals(type, "Studios"))
+            {
+                item = await LibraryManager.GetStudio(name).ConfigureAwait(false);
+            }
+            else
+            {
+                throw new ArgumentException();
+            }
+
+            var key = item.GetUserDataKey();
+            
             // Get the user data for this item
             var data = await UserDataRepository.GetUserData(userId, key).ConfigureAwait(false);
 
@@ -216,11 +246,37 @@ namespace MediaBrowser.Api.UserLibrary
         /// Marks the like.
         /// </summary>
         /// <param name="userId">The user id.</param>
-        /// <param name="key">The key.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="name">The name.</param>
         /// <param name="likes">if set to <c>true</c> [likes].</param>
         /// <returns>Task.</returns>
-        protected async Task MarkLike(Guid userId, string key, bool? likes)
+        protected async Task MarkLike(Guid userId, string type, string name, bool? likes)
         {
+            BaseItem item;
+
+            if (string.Equals(type, "Persons"))
+            {
+                item = await LibraryManager.GetPerson(name).ConfigureAwait(false);
+            }
+            else if (string.Equals(type, "Artists"))
+            {
+                item = await LibraryManager.GetArtist(name).ConfigureAwait(false);
+            }
+            else if (string.Equals(type, "Genres"))
+            {
+                item = await LibraryManager.GetGenre(name).ConfigureAwait(false);
+            }
+            else if (string.Equals(type, "Studios"))
+            {
+                item = await LibraryManager.GetStudio(name).ConfigureAwait(false);
+            }
+            else
+            {
+                throw new ArgumentException();
+            }
+
+            var key = item.GetUserDataKey();
+            
             // Get the user data for this item
             var data = await UserDataRepository.GetUserData(userId, key).ConfigureAwait(false);
 

+ 8 - 1
MediaBrowser.Api/UserLibrary/ItemsService.cs

@@ -21,6 +21,13 @@ namespace MediaBrowser.Api.UserLibrary
     [Api(Description = "Gets items based on a query.")]
     public class GetItems : BaseItemsRequest, IReturn<ItemsResult>
     {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public Guid UserId { get; set; }
+
         /// <summary>
         /// Limit results to items containing a specific person
         /// </summary>
@@ -328,7 +335,7 @@ namespace MediaBrowser.Api.UserLibrary
                     });
 
                 case ItemFilter.IsRecentlyAdded:
-                    return items.Where(item => item.IsRecentlyAdded(currentUser));
+                    return items.Where(item => item.IsRecentlyAdded());
 
                 case ItemFilter.IsResumable:
                     return items.Where(item =>

+ 65 - 9
MediaBrowser.Api/UserLibrary/PersonsService.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
@@ -6,6 +7,7 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
 using ServiceStack.ServiceHost;
 using System;
 using System.Collections.Generic;
@@ -17,8 +19,7 @@ namespace MediaBrowser.Api.UserLibrary
     /// <summary>
     /// Class GetPersons
     /// </summary>
-    [Route("/Users/{UserId}/Items/{ParentId}/Persons", "GET")]
-    [Route("/Users/{UserId}/Items/Root/Persons", "GET")]
+    [Route("/Persons", "GET")]
     [Api(Description = "Gets all persons from a given item, folder, or the entire library")]
     public class GetPersons : GetItemsByName
     {
@@ -32,7 +33,7 @@ namespace MediaBrowser.Api.UserLibrary
     /// <summary>
     /// Class GetPersonItemCounts
     /// </summary>
-    [Route("/Users/{UserId}/Persons/{Name}/Counts", "GET")]
+    [Route("/Persons/{Name}/Counts", "GET")]
     [Api(Description = "Gets item counts of library items that a person appears in")]
     public class GetPersonItemCounts : IReturn<ItemByNameCounts>
     {
@@ -51,6 +52,28 @@ namespace MediaBrowser.Api.UserLibrary
         public string Name { get; set; }
     }
 
+    /// <summary>
+    /// Class GetPerson
+    /// </summary>
+    [Route("/Persons/{Name}", "GET")]
+    [Api(Description = "Gets a person, by name")]
+    public class GetPerson : IReturn<BaseItemDto>
+    {
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        [ApiMember(Name = "Name", Description = "The person name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Name { get; set; }
+
+        /// <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>
     /// Class PersonsService
     /// </summary>
@@ -67,6 +90,42 @@ namespace MediaBrowser.Api.UserLibrary
         {
         }
 
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetPerson request)
+        {
+            var result = GetItem(request).Result;
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the item.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>Task{BaseItemDto}.</returns>
+        private async Task<BaseItemDto> GetItem(GetPerson request)
+        {
+            var item = await LibraryManager.GetPerson(request.Name).ConfigureAwait(false);
+
+            // Get everything
+            var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true));
+
+            var builder = new DtoBuilder(Logger, LibraryManager, UserDataRepository);
+
+            if (request.UserId.HasValue)
+            {
+                var user = UserManager.GetUserById(request.UserId.Value);
+
+                return await builder.GetBaseItemDto(item, user, fields.ToList()).ConfigureAwait(false);
+            }
+
+            return await builder.GetBaseItemDto(item, fields.ToList()).ConfigureAwait(false);
+        }
+
         /// <summary>
         /// Gets the specified request.
         /// </summary>
@@ -86,9 +145,7 @@ namespace MediaBrowser.Api.UserLibrary
         /// <returns>System.Object.</returns>
         public object Get(GetPersonItemCounts request)
         {
-            var user = UserManager.GetUserById(request.UserId);
-
-            var items = user.RootFolder.GetRecursiveChildren(user).Where(i => i.People != null && i.People.Any(p => string.Equals(p.Name, request.Name, StringComparison.OrdinalIgnoreCase))).ToList();
+            var items = GetItems(request.UserId).Where(i => i.People != null && i.People.Any(p => string.Equals(p.Name, request.Name, StringComparison.OrdinalIgnoreCase))).ToList();
 
             var counts = new ItemByNameCounts
             {
@@ -117,9 +174,8 @@ namespace MediaBrowser.Api.UserLibrary
         /// </summary>
         /// <param name="request">The request.</param>
         /// <param name="items">The items.</param>
-        /// <param name="user">The user.</param>
         /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
-        protected override IEnumerable<IbnStub<Person>> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items, User user)
+        protected override IEnumerable<IbnStub<Person>> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items)
         {
             var inputPersonTypes = ((GetPersons)request).PersonTypes;
             var personTypes = string.IsNullOrEmpty(inputPersonTypes) ? new string[] { } : inputPersonTypes.Split(',');

+ 66 - 10
MediaBrowser.Api/UserLibrary/StudiosService.cs

@@ -1,10 +1,12 @@
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
 using ServiceStack.ServiceHost;
 using System;
 using System.Collections.Generic;
@@ -16,14 +18,13 @@ namespace MediaBrowser.Api.UserLibrary
     /// <summary>
     /// Class GetStudios
     /// </summary>
-    [Route("/Users/{UserId}/Items/{ParentId}/Studios", "GET")]
-    [Route("/Users/{UserId}/Items/Root/Studios", "GET")]
+    [Route("/Studios", "GET")]
     [Api(Description = "Gets all studios from a given item, folder, or the entire library")]
     public class GetStudios : GetItemsByName
     {
     }
 
-    [Route("/Users/{UserId}/Studios/{Name}/Counts", "GET")]
+    [Route("/Studios/{Name}/Counts", "GET")]
     [Api(Description = "Gets item counts of library items that a studio appears in")]
     public class GetStudioItemCounts : IReturn<ItemByNameCounts>
     {
@@ -42,6 +43,28 @@ namespace MediaBrowser.Api.UserLibrary
         public string Name { get; set; }
     }
 
+    /// <summary>
+    /// Class GetStudio
+    /// </summary>
+    [Route("/Studios/{Name}", "GET")]
+    [Api(Description = "Gets a studio, by name")]
+    public class GetStudio : IReturn<BaseItemDto>
+    {
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        [ApiMember(Name = "Name", Description = "The studio name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Name { get; set; }
+
+        /// <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>
     /// Class StudiosService
     /// </summary>
@@ -57,11 +80,45 @@ namespace MediaBrowser.Api.UserLibrary
         /// </summary>
         /// <param name="request">The request.</param>
         /// <returns>System.Object.</returns>
-        public object Get(GetStudioItemCounts request)
+        public object Get(GetStudio request)
+        {
+            var result = GetItem(request).Result;
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the item.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>Task{BaseItemDto}.</returns>
+        private async Task<BaseItemDto> GetItem(GetStudio request)
         {
-            var user = UserManager.GetUserById(request.UserId);
+            var item = await LibraryManager.GetStudio(request.Name).ConfigureAwait(false);
+
+            // Get everything
+            var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true));
+
+            var builder = new DtoBuilder(Logger, LibraryManager, UserDataRepository);
+
+            if (request.UserId.HasValue)
+            {
+                var user = UserManager.GetUserById(request.UserId.Value);
+
+                return await builder.GetBaseItemDto(item, user, fields.ToList()).ConfigureAwait(false);
+            }
 
-            var items = user.RootFolder.GetRecursiveChildren(user).Where(i => i.Studios != null && i.Studios.Contains(request.Name, StringComparer.OrdinalIgnoreCase)).ToList();
+            return await builder.GetBaseItemDto(item, fields.ToList()).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetStudioItemCounts request)
+        {
+            var items = GetItems(request.UserId).Where(i => i.Studios != null && i.Studios.Contains(request.Name, StringComparer.OrdinalIgnoreCase)).ToList();
 
             var counts = new ItemByNameCounts
             {
@@ -94,15 +151,14 @@ namespace MediaBrowser.Api.UserLibrary
 
             return ToOptimizedResult(result);
         }
-        
+
         /// <summary>
         /// Gets all items.
         /// </summary>
         /// <param name="request">The request.</param>
         /// <param name="items">The items.</param>
-        /// <param name="user">The user.</param>
         /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
-        protected override IEnumerable<IbnStub<Studio>> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items, User user)
+        protected override IEnumerable<IbnStub<Studio>> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items)
         {
             var itemsList = items.Where(i => i.Studios != null).ToList();
 

+ 65 - 5
MediaBrowser.Api/UserLibrary/YearsService.cs

@@ -1,7 +1,11 @@
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
 using ServiceStack.ServiceHost;
+using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
@@ -12,13 +16,34 @@ namespace MediaBrowser.Api.UserLibrary
     /// <summary>
     /// Class GetYears
     /// </summary>
-    [Route("/Users/{UserId}/Items/{ParentId}/Years", "GET")]
-    [Route("/Users/{UserId}/Items/Root/Years", "GET")]
+    [Route("/Years", "GET")]
     [Api(Description = "Gets all years from a given item, folder, or the entire library")]
     public class GetYears : GetItemsByName
     {
     }
 
+    /// <summary>
+    /// Class GetYear
+    /// </summary>
+    [Route("/Years/{Year}", "GET")]
+    [Api(Description = "Gets a year")]
+    public class GetYear : IReturn<BaseItemDto>
+    {
+        /// <summary>
+        /// Gets or sets the year.
+        /// </summary>
+        /// <value>The year.</value>
+        [ApiMember(Name = "Year", Description = "The year", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
+        public int Year { get; set; }
+
+        /// <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>
     /// Class YearsService
     /// </summary>
@@ -34,6 +59,42 @@ namespace MediaBrowser.Api.UserLibrary
         {
         }
 
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetYear request)
+        {
+            var result = GetItem(request).Result;
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the item.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>Task{BaseItemDto}.</returns>
+        private async Task<BaseItemDto> GetItem(GetYear request)
+        {
+            var item = await LibraryManager.GetYear(request.Year).ConfigureAwait(false);
+
+            // Get everything
+            var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true));
+
+            var builder = new DtoBuilder(Logger, LibraryManager, UserDataRepository);
+
+            if (request.UserId.HasValue)
+            {
+                var user = UserManager.GetUserById(request.UserId.Value);
+
+                return await builder.GetBaseItemDto(item, user, fields.ToList()).ConfigureAwait(false);
+            }
+
+            return await builder.GetBaseItemDto(item, fields.ToList()).ConfigureAwait(false);
+        }
+
         /// <summary>
         /// Gets the specified request.
         /// </summary>
@@ -51,9 +112,8 @@ namespace MediaBrowser.Api.UserLibrary
         /// </summary>
         /// <param name="request">The request.</param>
         /// <param name="items">The items.</param>
-        /// <param name="user">The user.</param>
         /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
-        protected override IEnumerable<IbnStub<Year>> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items, User user)
+        protected override IEnumerable<IbnStub<Year>> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items)
         {
             var itemsList = items.Where(i => i.ProductionYear != null).ToList();
 

+ 1 - 1
MediaBrowser.Controller/Dto/DtoBuilder.cs

@@ -528,7 +528,7 @@ namespace MediaBrowser.Controller.Dto
                 recursiveItemCount++;
 
                 // Check is recently added
-                if (child.IsRecentlyAdded(user))
+                if (child.IsRecentlyAdded())
                 {
                     rcentlyAddedItemCount++;
                 }

+ 1 - 1
MediaBrowser.Controller/Entities/Audio/Artist.cs

@@ -12,7 +12,7 @@ namespace MediaBrowser.Controller.Entities.Audio
         /// <returns>System.String.</returns>
         public override string GetUserDataKey()
         {
-            return Name;
+            return "Artist-" + Name;
         }
     }
 }

+ 10 - 0
MediaBrowser.Controller/Entities/Audio/Audio.cs

@@ -113,5 +113,15 @@ namespace MediaBrowser.Controller.Entities.Audio
             return (ProductionYear != null ? ProductionYear.Value.ToString("000-") : "")
                     + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name;
         }
+
+        /// <summary>
+        /// Determines whether the specified name has artist.
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <returns><c>true</c> if the specified name has artist; otherwise, <c>false</c>.</returns>
+        public bool HasArtist(string name)
+        {
+            return Artists.Contains(name, StringComparer.OrdinalIgnoreCase) || string.Equals(AlbumArtist, name, StringComparison.OrdinalIgnoreCase);
+        }
     }
 }

+ 1 - 1
MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs

@@ -40,7 +40,7 @@ namespace MediaBrowser.Controller.Entities.Audio
         /// <summary>
         /// The unknwon artist
         /// </summary>
-        private static readonly MusicArtist UnknwonArtist = new MusicArtist {Name = "<Unknown>"};
+        private static readonly MusicArtist UnknwonArtist = new MusicArtist { Name = "<Unknown>" };
 
         /// <summary>
         /// Override this to return the folder that should be used to construct a container

+ 0 - 8
MediaBrowser.Controller/Entities/Audio/MusicArtist.cs

@@ -1,6 +1,4 @@
 
-using System.Collections.Generic;
-
 namespace MediaBrowser.Controller.Entities.Audio
 {
     /// <summary>
@@ -8,12 +6,6 @@ namespace MediaBrowser.Controller.Entities.Audio
     /// </summary>
     public class MusicArtist : Folder
     {
-        public Dictionary<string, string> AlbumCovers { get; set; }
 
-        public override void ClearMetaValues()
-        {
-            AlbumCovers = null;
-            base.ClearMetaValues();
-        }
     }
 }

+ 9 - 7
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -23,6 +23,14 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public abstract class BaseItem : IHasProviderIds
     {
+        protected BaseItem()
+        {
+            Genres = new List<string>();
+            TrailerUrls = new List<string>();
+            Studios = new List<string>();
+            People = new List<PersonInfo>();
+        }
+
         /// <summary>
         /// The trailer folder name
         /// </summary>
@@ -925,16 +933,10 @@ namespace MediaBrowser.Controller.Entities
         /// <summary>
         /// Determines if the item is considered new based on user settings
         /// </summary>
-        /// <param name="user">The user.</param>
         /// <returns><c>true</c> if [is recently added] [the specified user]; otherwise, <c>false</c>.</returns>
         /// <exception cref="System.ArgumentNullException"></exception>
-        public bool IsRecentlyAdded(User user)
+        public bool IsRecentlyAdded()
         {
-            if (user == null)
-            {
-                throw new ArgumentNullException();
-            }
-
             return (DateTime.UtcNow - DateCreated).TotalDays < ConfigurationManager.Configuration.RecentItemDays;
         }
 

+ 1 - 1
MediaBrowser.Controller/Entities/Genre.cs

@@ -12,7 +12,7 @@ namespace MediaBrowser.Controller.Entities
         /// <returns>System.String.</returns>
         public override string GetUserDataKey()
         {
-            return Name;
+            return "Genre-" + Name;
         }
     }
 }

+ 1 - 1
MediaBrowser.Controller/Entities/Person.cs

@@ -12,7 +12,7 @@ namespace MediaBrowser.Controller.Entities
         /// <returns>System.String.</returns>
         public override string GetUserDataKey()
         {
-            return Name;
+            return "Person-" + Name;
         }
     }
 

+ 1 - 1
MediaBrowser.Controller/Entities/Studio.cs

@@ -12,7 +12,7 @@ namespace MediaBrowser.Controller.Entities
         /// <returns>System.String.</returns>
         public override string GetUserDataKey()
         {
-            return Name;
+            return "Studio-" + Name;
         }
     }
 }

+ 1 - 1
MediaBrowser.Controller/Entities/Year.cs

@@ -12,7 +12,7 @@ namespace MediaBrowser.Controller.Entities
         /// <returns>System.String.</returns>
         public override string GetUserDataKey()
         {
-            return Name;
+            return "Year-" + Name;
         }
     }
 }

+ 8 - 0
MediaBrowser.Controller/Library/ILibraryManager.cs

@@ -213,5 +213,13 @@ namespace MediaBrowser.Controller.Library
         /// <param name="parent">The parent.</param>
         /// <returns>IEnumerable{BaseItem}.</returns>
         IEnumerable<BaseItem> RetrieveChildren(Folder parent);
+
+        /// <summary>
+        /// Validates the artists.
+        /// </summary>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="progress">The progress.</param>
+        /// <returns>Task.</returns>
+        Task ValidateArtists(CancellationToken cancellationToken, IProgress<double> progress);
     }
 }

+ 9 - 0
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -53,8 +53,12 @@
     <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
   </PropertyGroup>
   <ItemGroup>
+    <Reference Include="MoreLinq">
+      <HintPath>..\packages\morelinq.1.0.15631-beta\lib\net35\MoreLinq.dll</HintPath>
+    </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
+    <Reference Include="System.Data" />
     <Reference Include="System.Drawing" />
     <Reference Include="System.Net" />
     <Reference Include="System.Runtime.Serialization" />
@@ -111,9 +115,11 @@
     <Compile Include="Providers\IProviderManager.cs" />
     <Compile Include="Providers\MediaInfo\MediaEncoderHelpers.cs" />
     <Compile Include="Providers\MetadataProviderPriority.cs" />
+    <Compile Include="Providers\Music\FanArtArtistByNameProvider.cs" />
     <Compile Include="Providers\Music\LastfmAlbumProvider.cs" />
     <Compile Include="Providers\Music\FanArtAlbumProvider.cs" />
     <Compile Include="Providers\Music\FanArtArtistProvider.cs" />
+    <Compile Include="Providers\Music\LastfmArtistByNameProvider.cs" />
     <Compile Include="Providers\Music\LastfmArtistProvider.cs" />
     <Compile Include="Providers\Music\LastfmHelper.cs" />
     <Compile Include="Providers\Music\MusicArtistProviderFromJson.cs" />
@@ -197,6 +203,9 @@
       <Name>MediaBrowser.Model</Name>
     </ProjectReference>
   </ItemGroup>
+  <ItemGroup>
+    <None Include="packages.config" />
+  </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <PropertyGroup>
     <PostBuildEvent>if $(ConfigurationName) == Release (

+ 2 - 1
MediaBrowser.Controller/Providers/IProviderManager.cs

@@ -14,11 +14,12 @@ namespace MediaBrowser.Controller.Providers
         /// <param name="item">The item.</param>
         /// <param name="source">The source.</param>
         /// <param name="targetName">Name of the target.</param>
+        /// <param name="saveLocally">if set to <c>true</c> [save locally].</param>
         /// <param name="resourcePool">The resource pool.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{System.String}.</returns>
         /// <exception cref="System.ArgumentNullException">item</exception>
-        Task<string> DownloadAndSaveImage(BaseItem item, string source, string targetName, SemaphoreSlim resourcePool, CancellationToken cancellationToken);
+        Task<string> DownloadAndSaveImage(BaseItem item, string source, string targetName, bool saveLocally, SemaphoreSlim resourcePool, CancellationToken cancellationToken);
 
         /// <summary>
         /// Saves to library filesystem.

+ 5 - 5
MediaBrowser.Controller/Providers/Movies/FanArtMovieProvider.cs

@@ -150,7 +150,7 @@ namespace MediaBrowser.Controller.Providers.Movies
                             Logger.Debug("FanArtProvider getting ClearLogo for " + movie.Name);
                             try
                             {
-                                movie.SetImage(ImageType.Logo, await _providerManager.DownloadAndSaveImage(movie, path, LOGO_FILE, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                                movie.SetImage(ImageType.Logo, await _providerManager.DownloadAndSaveImage(movie, path, LOGO_FILE, ConfigurationManager.Configuration.SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
                             }
                             catch (HttpException)
                             {
@@ -176,7 +176,7 @@ namespace MediaBrowser.Controller.Providers.Movies
                             Logger.Debug("FanArtProvider getting ClearArt for " + movie.Name);
                             try
                             {
-                                movie.SetImage(ImageType.Art, await _providerManager.DownloadAndSaveImage(movie, path, ART_FILE, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                                movie.SetImage(ImageType.Art, await _providerManager.DownloadAndSaveImage(movie, path, ART_FILE, ConfigurationManager.Configuration.SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
                             }
                             catch (HttpException)
                             {
@@ -199,7 +199,7 @@ namespace MediaBrowser.Controller.Providers.Movies
                             Logger.Debug("FanArtProvider getting DiscArt for " + movie.Name);
                             try
                             {
-                                movie.SetImage(ImageType.Disc, await _providerManager.DownloadAndSaveImage(movie, path, DISC_FILE, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                                movie.SetImage(ImageType.Disc, await _providerManager.DownloadAndSaveImage(movie, path, DISC_FILE, ConfigurationManager.Configuration.SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
                             }
                             catch (HttpException)
                             {
@@ -223,7 +223,7 @@ namespace MediaBrowser.Controller.Providers.Movies
                             Logger.Debug("FanArtProvider getting Banner for " + movie.Name);
                             try
                             {
-                                movie.SetImage(ImageType.Banner, await _providerManager.DownloadAndSaveImage(movie, path, BANNER_FILE, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                                movie.SetImage(ImageType.Banner, await _providerManager.DownloadAndSaveImage(movie, path, BANNER_FILE, ConfigurationManager.Configuration.SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
                             }
                             catch (HttpException)
                             {
@@ -247,7 +247,7 @@ namespace MediaBrowser.Controller.Providers.Movies
                             Logger.Debug("FanArtProvider getting Banner for " + movie.Name);
                             try
                             {
-                                movie.SetImage(ImageType.Thumb, await _providerManager.DownloadAndSaveImage(movie, path, THUMB_FILE, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                                movie.SetImage(ImageType.Thumb, await _providerManager.DownloadAndSaveImage(movie, path, THUMB_FILE, ConfigurationManager.Configuration.SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
                             }
                             catch (HttpException)
                             {

+ 4 - 7
MediaBrowser.Controller/Providers/Movies/MovieDbProvider.cs

@@ -251,7 +251,7 @@ namespace MediaBrowser.Controller.Providers.Movies
             new Regex(@"(?<name>.*)") // last resort matches the whole string as the name
         };
 
-        public const string LOCAL_META_FILE_NAME = "MBMovie.json";
+        public const string LOCAL_META_FILE_NAME = "mbmovie.js";
         public const string ALT_META_FILE_NAME = "movie.xml";
         protected string ItemType = "movie";
 
@@ -268,7 +268,7 @@ namespace MediaBrowser.Controller.Providers.Movies
 
             }
 
-            if (providerInfo.LastRefreshStatus == ProviderRefreshStatus.CompletedWithErrors)
+            if (providerInfo.LastRefreshStatus != ProviderRefreshStatus.Success)
             {
                 Logger.Debug("MovieProvider for {0} - last attempt had errors.  Will try again.", item.Path);
                 return true;
@@ -281,9 +281,6 @@ namespace MediaBrowser.Controller.Providers.Movies
                 return false;
             }
 
-            if (DateTime.Today.Subtract(item.DateCreated).TotalDays > 180 && downloadDate != DateTime.MinValue)
-                return false; // don't trigger a refresh data for item that are more than 6 months old and have been refreshed before
-
             if (DateTime.Today.Subtract(downloadDate).TotalDays < ConfigurationManager.Configuration.MetadataRefreshDays) // only refresh every n days
                 return false;
 
@@ -1034,7 +1031,7 @@ namespace MediaBrowser.Controller.Providers.Movies
                 {
                     try
                     {
-                        item.PrimaryImagePath = await ProviderManager.DownloadAndSaveImage(item, tmdbImageUrl + poster.file_path, "folder" + Path.GetExtension(poster.file_path), MovieDbResourcePool, cancellationToken).ConfigureAwait(false);
+                        item.PrimaryImagePath = await ProviderManager.DownloadAndSaveImage(item, tmdbImageUrl + poster.file_path, "folder" + Path.GetExtension(poster.file_path), ConfigurationManager.Configuration.SaveLocalMeta, MovieDbResourcePool, cancellationToken).ConfigureAwait(false);
                     }
                     catch (HttpException)
                     {
@@ -1066,7 +1063,7 @@ namespace MediaBrowser.Controller.Providers.Movies
                     {
                         try
                         {
-                            item.BackdropImagePaths.Add(await ProviderManager.DownloadAndSaveImage(item, tmdbImageUrl + images.backdrops[i].file_path, bdName + Path.GetExtension(images.backdrops[i].file_path), MovieDbResourcePool, cancellationToken).ConfigureAwait(false));
+                            item.BackdropImagePaths.Add(await ProviderManager.DownloadAndSaveImage(item, tmdbImageUrl + images.backdrops[i].file_path, bdName + Path.GetExtension(images.backdrops[i].file_path), ConfigurationManager.Configuration.SaveLocalMeta, MovieDbResourcePool, cancellationToken).ConfigureAwait(false));
                         }
                         catch (HttpException)
                         {

+ 1 - 1
MediaBrowser.Controller/Providers/Movies/TmdbPersonProvider.cs

@@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Providers.Movies
         /// <summary>
         /// The meta file name
         /// </summary>
-        protected const string MetaFileName = "MBPerson.json";
+        protected const string MetaFileName = "mbperson.js";
 
         protected readonly IProviderManager ProviderManager;
         

+ 73 - 33
MediaBrowser.Controller/Providers/Music/FanArtAlbumProvider.cs

@@ -1,29 +1,34 @@
-using System.Collections.Generic;
-using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
 using System;
+using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
+using System.Xml;
 
 namespace MediaBrowser.Controller.Providers.Music
 {
     public class FanArtAlbumProvider : FanartBaseProvider
     {
         private readonly IProviderManager _providerManager;
-        
-        public FanArtAlbumProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager)
+
+        protected IHttpClient HttpClient { get; private set; }
+
+        public FanArtAlbumProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager)
             : base(logManager, configurationManager)
         {
             _providerManager = providerManager;
+            HttpClient = httpClient;
         }
 
         public override bool Supports(BaseItem item)
         {
-            return item is MusicAlbum && item.Parent is MusicArtist;
+            return item is MusicAlbum;
         }
 
         /// <summary>
@@ -35,7 +40,7 @@ namespace MediaBrowser.Controller.Providers.Music
         protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
         {
             //we fetch if image needed and haven't already tried recently
-            return string.IsNullOrEmpty(item.PrimaryImagePath) && 
+            return (string.IsNullOrEmpty(item.PrimaryImagePath) || !item.HasImage(ImageType.Disc)) &&
                    DateTime.Today.Subtract(providerInfo.LastRefreshed).TotalDays > ConfigurationManager.Configuration.MetadataRefreshDays;
         }
 
@@ -45,46 +50,81 @@ namespace MediaBrowser.Controller.Providers.Music
             if (mbid == null)
             {
                 Logger.Warn("No Musicbrainz id associated with album {0}", item.Name);
-                SetLastRefreshed(item, DateTime.UtcNow, ProviderRefreshStatus.CompletedWithErrors);
-                return false;
+                SetLastRefreshed(item, DateTime.UtcNow);
+                return true;
             }
 
             cancellationToken.ThrowIfCancellationRequested();
 
-            //Look at our parent for our album cover
-            var artist = (MusicArtist)item.Parent;
+            var url = string.Format("http://api.fanart.tv/webservice/album/{0}/{1}/xml/all/1/1", APIKey, item.GetProviderId(MetadataProviders.Musicbrainz));
 
-            var cover = artist.AlbumCovers != null ? GetValueOrDefault(artist.AlbumCovers, mbid, null) : null;
+            var doc = new XmlDocument();
 
-            if (cover == null)
+            try
+            {
+                using (var xml = await HttpClient.Get(url, FanArtResourcePool, cancellationToken).ConfigureAwait(false))
+                {
+                    doc.Load(xml);
+                }
+            }
+            catch (HttpException)
             {
-                Logger.Warn("Unable to find cover art for {0}", item.Name);
-                SetLastRefreshed(item, DateTime.UtcNow, ProviderRefreshStatus.CompletedWithErrors);
-                return false;
             }
 
-            item.SetImage(ImageType.Primary, await _providerManager.DownloadAndSaveImage(item, cover, "folder.jpg", FanArtResourcePool, cancellationToken).ConfigureAwait(false));
-            return true;
-        }
+            cancellationToken.ThrowIfCancellationRequested();
 
-        /// <summary>
-        /// Helper method for Dictionaries since they throw on not-found keys
-        /// </summary>
-        /// <typeparam name="T"></typeparam>
-        /// <typeparam name="U"></typeparam>
-        /// <param name="dictionary">The dictionary.</param>
-        /// <param name="key">The key.</param>
-        /// <param name="defaultValue">The default value.</param>
-        /// <returns>``1.</returns>
-        private static U GetValueOrDefault<T, U>(Dictionary<T, U> dictionary, T key, U defaultValue)
-        {
-            U val;
-            if (!dictionary.TryGetValue(key, out val))
+            if (doc.HasChildNodes)
             {
-                val = defaultValue;
+                if (ConfigurationManager.Configuration.DownloadMusicAlbumImages.Disc && !item.ResolveArgs.ContainsMetaFileByName(DISC_FILE))
+                {
+                    var node = doc.SelectSingleNode("//fanart/music/albums/album//cdart/@url");
+
+                    var path = node != null ? node.Value : null;
+
+                    if (!string.IsNullOrEmpty(path))
+                    {
+                        Logger.Debug("FanArtProvider getting Disc for " + item.Name);
+                        try
+                        {
+                            item.SetImage(ImageType.Disc, await _providerManager.DownloadAndSaveImage(item, path, DISC_FILE, ConfigurationManager.Configuration.SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                        }
+                        catch (HttpException)
+                        {
+                        }
+                        catch (IOException)
+                        {
+
+                        }
+                    }
+                }
+
+                if (ConfigurationManager.Configuration.DownloadMusicAlbumImages.Primary && !item.ResolveArgs.ContainsMetaFileByName(PRIMARY_FILE))
+                {
+                    var node = doc.SelectSingleNode("//fanart/music/albums/album//albumcover/@url");
+
+                    var path = node != null ? node.Value : null;
+
+                    if (!string.IsNullOrEmpty(path))
+                    {
+                        Logger.Debug("FanArtProvider getting albumcover for " + item.Name);
+                        try
+                        {
+                            item.SetImage(ImageType.Primary, await _providerManager.DownloadAndSaveImage(item, path, PRIMARY_FILE, ConfigurationManager.Configuration.SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                        }
+                        catch (HttpException)
+                        {
+                        }
+                        catch (IOException)
+                        {
+
+                        }
+                    }
+                }
             }
-            return val;
 
+            SetLastRefreshed(item, DateTime.UtcNow);
+
+            return true;
         }
     }
 }

+ 48 - 0
MediaBrowser.Controller/Providers/Music/FanArtArtistByNameProvider.cs

@@ -0,0 +1,48 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Model.Logging;
+
+namespace MediaBrowser.Controller.Providers.Music
+{
+    /// <summary>
+    /// Class FanArtArtistByNameProvider
+    /// </summary>
+    public class FanArtArtistByNameProvider : FanArtArtistProvider
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="FanArtArtistByNameProvider" /> class.
+        /// </summary>
+        /// <param name="httpClient">The HTTP client.</param>
+        /// <param name="logManager">The log manager.</param>
+        /// <param name="configurationManager">The configuration manager.</param>
+        /// <param name="providerManager">The provider manager.</param>
+        public FanArtArtistByNameProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager)
+            : base(httpClient, logManager, configurationManager, providerManager)
+        {
+        }
+
+        /// <summary>
+        /// Supportses the specified item.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+        public override bool Supports(BaseItem item)
+        {
+            return item is Artist;
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether [save local meta].
+        /// </summary>
+        /// <value><c>true</c> if [save local meta]; otherwise, <c>false</c>.</value>
+        protected override bool SaveLocalMeta
+        {
+            get
+            {
+                return true;
+            }
+        }
+    }
+}

+ 25 - 51
MediaBrowser.Controller/Providers/Music/FanArtArtistProvider.cs

@@ -18,7 +18,7 @@ namespace MediaBrowser.Controller.Providers.Music
     /// <summary>
     /// Class FanArtArtistProvider
     /// </summary>
-    class FanArtArtistProvider : FanartBaseProvider
+    public class FanArtArtistProvider : FanartBaseProvider
     {
         /// <summary>
         /// Gets the HTTP client.
@@ -54,6 +54,11 @@ namespace MediaBrowser.Controller.Providers.Music
             return item is MusicArtist;
         }
 
+        protected virtual bool SaveLocalMeta
+        {
+            get { return ConfigurationManager.Configuration.SaveLocalMeta; }
+        }
+
         /// <summary>
         /// Shoulds the fetch.
         /// </summary>
@@ -62,16 +67,11 @@ namespace MediaBrowser.Controller.Providers.Music
         /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
         protected override bool ShouldFetch(BaseItem item, BaseProviderInfo providerInfo)
         {
-            var artist = (MusicArtist)item;
-            if (item.Path == null || item.DontFetchMeta || string.IsNullOrEmpty(artist.GetProviderId(MetadataProviders.Musicbrainz))) return false; //nothing to do
-            var artExists = item.ResolveArgs.ContainsMetaFileByName(ART_FILE);
-            var logoExists = item.ResolveArgs.ContainsMetaFileByName(LOGO_FILE);
-            var discExists = item.ResolveArgs.ContainsMetaFileByName(DISC_FILE);
+            if (item.Path == null || item.DontFetchMeta || string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Musicbrainz))) return false; //nothing to do
 
-            return (!artExists && ConfigurationManager.Configuration.DownloadMusicArtistImages.Art)
-                || (!logoExists && ConfigurationManager.Configuration.DownloadMusicArtistImages.Logo)
-                || (!discExists && ConfigurationManager.Configuration.DownloadMusicArtistImages.Disc)
-                || ((artist.AlbumCovers == null || artist.AlbumCovers.Count == 0) && ConfigurationManager.Configuration.DownloadMusicAlbumImages.Primary);
+            return (!item.ResolveArgs.ContainsMetaFileByName(ART_FILE) && ConfigurationManager.Configuration.DownloadMusicArtistImages.Art)
+                || (!item.ResolveArgs.ContainsMetaFileByName(LOGO_FILE) && ConfigurationManager.Configuration.DownloadMusicArtistImages.Logo)
+                || (!item.ResolveArgs.ContainsMetaFileByName(DISC_FILE) && ConfigurationManager.Configuration.DownloadMusicArtistImages.Disc);
         }
 
         /// <summary>
@@ -85,7 +85,7 @@ namespace MediaBrowser.Controller.Providers.Music
         {
             cancellationToken.ThrowIfCancellationRequested();
 
-            var artist = (MusicArtist)item;
+            //var artist = item;
 
             BaseProviderInfo providerData;
 
@@ -94,9 +94,9 @@ namespace MediaBrowser.Controller.Providers.Music
                 providerData = new BaseProviderInfo();
             }
 
-            if (ShouldFetch(artist, providerData))
+            if (ShouldFetch(item, providerData))
             {
-                var url = string.Format(FanArtBaseUrl, APIKey, artist.GetProviderId(MetadataProviders.Musicbrainz));
+                var url = string.Format(FanArtBaseUrl, APIKey, item.GetProviderId(MetadataProviders.Musicbrainz));
                 var doc = new XmlDocument();
 
                 try
@@ -124,10 +124,10 @@ namespace MediaBrowser.Controller.Providers.Music
                         path = node != null ? node.Value : null;
                         if (!string.IsNullOrEmpty(path))
                         {
-                            Logger.Debug("FanArtProvider getting ClearLogo for " + artist.Name);
+                            Logger.Debug("FanArtProvider getting ClearLogo for " + item.Name);
                             try
                             {
-                                artist.SetImage(ImageType.Logo, await _providerManager.DownloadAndSaveImage(artist, path, LOGO_FILE, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                                item.SetImage(ImageType.Logo, await _providerManager.DownloadAndSaveImage(item, path, LOGO_FILE, SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
                             }
                             catch (HttpException)
                             {
@@ -146,16 +146,16 @@ namespace MediaBrowser.Controller.Providers.Music
                         if (nodes != null)
                         {
                             var numBackdrops = 0;
-                            artist.BackdropImagePaths = new List<string>();
+                            item.BackdropImagePaths = new List<string>();
                             foreach (XmlNode node in nodes)
                             {
                                 path = node.Value;
                                 if (!string.IsNullOrEmpty(path))
                                 {
-                                    Logger.Debug("FanArtProvider getting Backdrop for " + artist.Name);
+                                    Logger.Debug("FanArtProvider getting Backdrop for " + item.Name);
                                     try
                                     {
-                                        artist.BackdropImagePaths.Add(await _providerManager.DownloadAndSaveImage(artist, path, ("Backdrop" + (numBackdrops > 0 ? numBackdrops.ToString() : "") + ".jpg"), FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                                        item.BackdropImagePaths.Add(await _providerManager.DownloadAndSaveImage(item, path, ("Backdrop" + (numBackdrops > 0 ? numBackdrops.ToString() : "") + ".jpg"), SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
                                         numBackdrops++;
                                         if (numBackdrops >= ConfigurationManager.Configuration.MaxBackdrops) break;
                                     }
@@ -175,32 +175,6 @@ namespace MediaBrowser.Controller.Providers.Music
 
                     cancellationToken.ThrowIfCancellationRequested();
 
-                    if (ConfigurationManager.Configuration.DownloadMusicAlbumImages.Primary)
-                    {
-                        var nodes = doc.SelectNodes("//fanart/music/albums/*");
-                        if (nodes != null)
-                        {
-                            artist.AlbumCovers = new Dictionary<string, string>();
-                            foreach (XmlNode node in nodes)
-                            {
-
-                                var key = node.Attributes["id"] != null ? node.Attributes["id"].Value : null;
-                                var cover = node.SelectSingleNode("albumcover/@url");
-                                path = cover != null ? cover.Value : null;
-
-                                if (!string.IsNullOrEmpty(path) && !string.IsNullOrEmpty(key))
-                                {
-                                    Logger.Debug("FanArtProvider getting Album Cover for " + artist.Name);
-                                    artist.AlbumCovers[key] = path;
-                                }
-                            }
-
-                        }
-
-                    }
-
-                    cancellationToken.ThrowIfCancellationRequested();
-
                     if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Art && !item.ResolveArgs.ContainsMetaFileByName(ART_FILE))
                     {
                         var node =
@@ -209,10 +183,10 @@ namespace MediaBrowser.Controller.Providers.Music
                         path = node != null ? node.Value : null;
                         if (!string.IsNullOrEmpty(path))
                         {
-                            Logger.Debug("FanArtProvider getting ClearArt for " + artist.Name);
+                            Logger.Debug("FanArtProvider getting ClearArt for " + item.Name);
                             try
                             {
-                                artist.SetImage(ImageType.Art, await _providerManager.DownloadAndSaveImage(artist, path, ART_FILE, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                                item.SetImage(ImageType.Art, await _providerManager.DownloadAndSaveImage(item, path, ART_FILE, SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
                             }
                             catch (HttpException)
                             {
@@ -232,10 +206,10 @@ namespace MediaBrowser.Controller.Providers.Music
                         path = node != null ? node.Value : null;
                         if (!string.IsNullOrEmpty(path))
                         {
-                            Logger.Debug("FanArtProvider getting Banner for " + artist.Name);
+                            Logger.Debug("FanArtProvider getting Banner for " + item.Name);
                             try
                             {
-                                artist.SetImage(ImageType.Banner, await _providerManager.DownloadAndSaveImage(artist, path, BANNER_FILE, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                                item.SetImage(ImageType.Banner, await _providerManager.DownloadAndSaveImage(item, path, BANNER_FILE, SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
                             }
                             catch (HttpException)
                             {
@@ -256,10 +230,10 @@ namespace MediaBrowser.Controller.Providers.Music
                         path = node != null ? node.Value : null;
                         if (!string.IsNullOrEmpty(path))
                         {
-                            Logger.Debug("FanArtProvider getting Primary image for " + artist.Name);
+                            Logger.Debug("FanArtProvider getting Primary image for " + item.Name);
                             try
                             {
-                                artist.SetImage(ImageType.Primary, await _providerManager.DownloadAndSaveImage(artist, path, PRIMARY_FILE, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                                item.SetImage(ImageType.Primary, await _providerManager.DownloadAndSaveImage(item, path, PRIMARY_FILE, SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
                             }
                             catch (HttpException)
                             {
@@ -272,7 +246,7 @@ namespace MediaBrowser.Controller.Providers.Music
                     }
                 }
             }
-            SetLastRefreshed(artist, DateTime.UtcNow);
+            SetLastRefreshed(item, DateTime.UtcNow);
             return true;
         }
     }

+ 59 - 26
MediaBrowser.Controller/Providers/Music/LastfmAlbumProvider.cs

@@ -1,14 +1,15 @@
-using System.IO;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Model.Logging;
-using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Serialization;
+using MoreLinq;
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
 
 namespace MediaBrowser.Controller.Providers.Music
 {
@@ -33,26 +34,7 @@ namespace MediaBrowser.Controller.Providers.Music
 
         protected override async Task FetchLastfmData(BaseItem item, string id, CancellationToken cancellationToken)
         {
-            // Get albu info using artist and album name
-            var url = RootUrl + string.Format("method=album.getInfo&artist={0}&album={1}&api_key={2}&format=json", UrlEncode(item.Parent.Name), UrlEncode(item.Name), ApiKey);
-
-            LastfmGetAlbumResult result;
-
-            try
-            {
-                using (var json = await HttpClient.Get(url, LastfmResourcePool, cancellationToken).ConfigureAwait(false))
-                {
-                    result = JsonSerializer.DeserializeFromStream<LastfmGetAlbumResult>(json);
-                }
-            }
-            catch (HttpException e)
-            {
-                if (e.StatusCode == HttpStatusCode.NotFound)
-                {
-                    throw new LastfmProviderException(string.Format("Unable to retrieve album info for {0} with artist {1}", item.Name, item.Parent.Name));
-                }
-                throw;
-            }
+            var result = await GetAlbumResult(item, cancellationToken).ConfigureAwait(false);
 
             if (result != null && result.album != null)
             {
@@ -71,9 +53,60 @@ namespace MediaBrowser.Controller.Providers.Music
             }
         }
 
+        private async Task<LastfmGetAlbumResult> GetAlbumResult(BaseItem item, CancellationToken cancellationToken)
+        {
+            var result = await GetAlbumResult(item.Parent.Name, item.Name, cancellationToken);
+
+            if (result != null && result.album != null)
+            {
+                return result;
+            }
+
+            var folder = (Folder)item;
+
+            // Get each song, distinct by the combination of AlbumArtist and Album
+            var songs = folder.Children.OfType<Audio>().DistinctBy(i => (i.AlbumArtist ?? string.Empty) + (i.Album ?? string.Empty), StringComparer.OrdinalIgnoreCase).ToList();
+
+            foreach (var song in songs.Where(song => !string.IsNullOrEmpty(song.Album) && !string.IsNullOrEmpty(song.AlbumArtist)))
+            {
+                result = await GetAlbumResult(song.AlbumArtist, song.Album, cancellationToken).ConfigureAwait(false);
+
+                if (result != null && result.album != null)
+                {
+                    return result;
+                }
+            }
+
+            return null;
+        }
+
+        private async Task<LastfmGetAlbumResult> GetAlbumResult(string artist, string album, CancellationToken cancellationToken)
+        {
+            // Get albu info using artist and album name
+            var url = RootUrl + string.Format("method=album.getInfo&artist={0}&album={1}&api_key={2}&format=json", UrlEncode(artist), UrlEncode(album), ApiKey);
+
+            using (var json = await HttpClient.Get(url, LastfmResourcePool, cancellationToken).ConfigureAwait(false))
+            {
+                return JsonSerializer.DeserializeFromStream<LastfmGetAlbumResult>(json);
+            }
+        }
+        
+        protected override Task FetchData(BaseItem item, CancellationToken cancellationToken)
+        {
+            return FetchLastfmData(item, string.Empty, cancellationToken);
+        }
+
         public override bool Supports(BaseItem item)
         {
             return item is MusicAlbum;
         }
+
+        protected override bool RefreshOnFileSystemStampChange
+        {
+            get
+            {
+                return true;
+            }
+        }
     }
 }

+ 50 - 0
MediaBrowser.Controller/Providers/Music/LastfmArtistByNameProvider.cs

@@ -0,0 +1,50 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Providers.Music
+{
+    /// <summary>
+    /// Class LastfmArtistByNameProvider
+    /// </summary>
+    public class LastfmArtistByNameProvider : LastfmArtistProvider
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LastfmArtistByNameProvider"/> class.
+        /// </summary>
+        /// <param name="jsonSerializer">The json serializer.</param>
+        /// <param name="httpClient">The HTTP client.</param>
+        /// <param name="logManager">The log manager.</param>
+        /// <param name="configurationManager">The configuration manager.</param>
+        /// <param name="providerManager">The provider manager.</param>
+        public LastfmArtistByNameProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager)
+            : base(jsonSerializer, httpClient, logManager, configurationManager, providerManager)
+        {
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether [save local meta].
+        /// </summary>
+        /// <value><c>true</c> if [save local meta]; otherwise, <c>false</c>.</value>
+        protected override bool SaveLocalMeta
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        /// <summary>
+        /// Supportses the specified item.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+        public override bool Supports(BaseItem item)
+        {
+            return item is Artist;
+        }
+    }
+}

+ 5 - 16
MediaBrowser.Controller/Providers/Music/LastfmArtistProvider.cs

@@ -29,7 +29,7 @@ namespace MediaBrowser.Controller.Providers.Music
             //Execute the Artist search against our name and assume first one is the one we want
             var url = RootUrl + string.Format("method=artist.search&artist={0}&api_key={1}&format=json", UrlEncode(item.Name), ApiKey);
 
-            LastfmArtistSearchResults searchResult = null;
+            LastfmArtistSearchResults searchResult;
 
             try
             {
@@ -60,29 +60,18 @@ namespace MediaBrowser.Controller.Providers.Music
             // Get artist info with provided id
             var url = RootUrl + string.Format("method=artist.getInfo&mbid={0}&api_key={1}&format=json", UrlEncode(id), ApiKey);
 
-            LastfmGetArtistResult result = null;
+            LastfmGetArtistResult result;
 
-            try
+            using (var json = await HttpClient.Get(url, LastfmResourcePool, cancellationToken).ConfigureAwait(false))
             {
-                using (var json = await HttpClient.Get(url, LastfmResourcePool, cancellationToken).ConfigureAwait(false))
-                {
-                    result = JsonSerializer.DeserializeFromStream<LastfmGetArtistResult>(json);
-                }
-            }
-            catch (HttpException e)
-            {
-                if (e.StatusCode == HttpStatusCode.NotFound)
-                {
-                    throw new LastfmProviderException(string.Format("Unable to retrieve artist info for {0} with id {1}", item.Name, id));
-                }
-                throw;
+                result = JsonSerializer.DeserializeFromStream<LastfmGetArtistResult>(json);
             }
 
             if (result != null && result.artist != null)
             {
                 LastfmHelper.ProcessArtistData(item, result.artist);
                 //And save locally if indicated
-                if (ConfigurationManager.Configuration.SaveLocalMeta)
+                if (SaveLocalMeta)
                 {
                     var ms = new MemoryStream();
                     JsonSerializer.SerializeToStream(result.artist, ms);

+ 19 - 60
MediaBrowser.Controller/Providers/Music/LastfmBaseProvider.cs

@@ -1,26 +1,17 @@
-using System.Collections.Generic;
-using System.Net;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
 using System;
+using System.Collections.Generic;
+using System.Net;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Model.Serialization;
 
 namespace MediaBrowser.Controller.Providers.Music
 {
-    class LastfmProviderException : ApplicationException
-    {
-        public LastfmProviderException(string msg)
-            : base(msg)
-        {
-        }
-     
-    }
     /// <summary>
     /// Class MovieDbProvider
     /// </summary>
@@ -84,6 +75,14 @@ namespace MediaBrowser.Controller.Providers.Music
         /// </summary>
         protected string LocalMetaFileName { get; set; }
 
+        protected virtual bool SaveLocalMeta
+        {
+            get
+            {
+                return ConfigurationManager.Configuration.SaveLocalMeta;
+            }
+        }
+
         /// <summary>
         /// If we save locally, refresh if they delete something
         /// </summary>
@@ -91,7 +90,7 @@ namespace MediaBrowser.Controller.Providers.Music
         {
             get
             {
-                return ConfigurationManager.Configuration.SaveLocalMeta;
+                return SaveLocalMeta;
             }
         }
 
@@ -173,16 +172,15 @@ namespace MediaBrowser.Controller.Providers.Music
         {
             if (item.DontFetchMeta) return false;
 
-            if (ConfigurationManager.Configuration.SaveLocalMeta && HasFileSystemStampChanged(item, providerInfo))
+            if (RefreshOnFileSystemStampChange && HasFileSystemStampChanged(item, providerInfo))
             {
                 //If they deleted something from file system, chances are, this item was mis-identified the first time
                 item.SetProviderId(MetadataProviders.Musicbrainz, null);
                 Logger.Debug("LastfmProvider reports file system stamp change...");
                 return true;
-
             }
 
-            if (providerInfo.LastRefreshStatus == ProviderRefreshStatus.CompletedWithErrors)
+            if (providerInfo.LastRefreshStatus != ProviderRefreshStatus.Success)
             {
                 Logger.Debug("LastfmProvider for {0} - last attempt had errors.  Will try again.", item.Path);
                 return true;
@@ -194,22 +192,10 @@ namespace MediaBrowser.Controller.Providers.Music
                 return true;
             }
 
-            var downloadDate = providerInfo.LastRefreshed;
-
-            if (ConfigurationManager.Configuration.MetadataRefreshDays == -1 && downloadDate != DateTime.MinValue)
-            {
-                return false;
-            }
-
-            if (DateTime.Today.Subtract(item.DateCreated).TotalDays > 180 && downloadDate != DateTime.MinValue)
-                return false; // don't trigger a refresh data for item that are more than 6 months old and have been refreshed before
-
-            if (DateTime.Today.Subtract(downloadDate).TotalDays < ConfigurationManager.Configuration.MetadataRefreshDays) // only refresh every n days
-                return false;
-
+            if (DateTime.UtcNow.Subtract(providerInfo.LastRefreshed).TotalDays > ConfigurationManager.Configuration.MetadataRefreshDays) // only refresh every n days
+                return true;
 
-            Logger.Debug("LastfmProvider - " + item.Name + " needs refresh.  Download date: " + downloadDate + " item created date: " + item.DateCreated + " Check for Update age: " + ConfigurationManager.Configuration.MetadataRefreshDays);
-            return true;
+            return false;
         }
 
         /// <summary>
@@ -221,36 +207,9 @@ namespace MediaBrowser.Controller.Providers.Music
         /// <returns>Task{System.Boolean}.</returns>
         public override async Task<bool> FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken)
         {
-            if (item.DontFetchMeta)
-            {
-                Logger.Info("LastfmProvider - Not fetching because requested to ignore " + item.Name);
-                return false;
-            }
-
             cancellationToken.ThrowIfCancellationRequested();
 
-            BaseProviderInfo providerData;
-
-            if (!item.ProviderData.TryGetValue(Id, out providerData))
-            {
-                providerData = new BaseProviderInfo();
-            }
-            
-            if (!ConfigurationManager.Configuration.SaveLocalMeta || !HasLocalMeta(item) || (force && !HasLocalMeta(item)) || (RefreshOnVersionChange && providerData.ProviderVersion != ProviderVersion))
-            {
-                try
-                {
-                    await FetchData(item, cancellationToken).ConfigureAwait(false);
-                    SetLastRefreshed(item, DateTime.UtcNow);
-                }
-                catch (LastfmProviderException)
-                {
-                    SetLastRefreshed(item, DateTime.UtcNow, ProviderRefreshStatus.CompletedWithErrors);
-                }
-
-                return true;
-            }
-            Logger.Debug("LastfmProvider not fetching because local meta exists for " + item.Name);
+            await FetchData(item, cancellationToken).ConfigureAwait(false);
             SetLastRefreshed(item, DateTime.UtcNow);
             return true;
         }

+ 13 - 7
MediaBrowser.Controller/Providers/Music/LastfmHelper.cs

@@ -6,20 +6,23 @@ namespace MediaBrowser.Controller.Providers.Music
 {
     public static class LastfmHelper
     {
-        public static string LocalArtistMetaFileName = "MBArtist.json";
-        public static string LocalAlbumMetaFileName = "MBAlbum.json";
+        public static string LocalArtistMetaFileName = "mbartist.js";
+        public static string LocalAlbumMetaFileName = "mbalbum.js";
 
         public static void ProcessArtistData(BaseItem artist, LastfmArtist data)
         {
-            var overview = data.bio != null ? data.bio.content : null;
-
-            artist.Overview = overview;
-
             var yearFormed = 0;
 
             if (data.bio != null)
             {
                 Int32.TryParse(data.bio.yearformed, out yearFormed);
+
+                artist.Overview = data.bio.content;
+
+                if (!string.IsNullOrEmpty(data.bio.placeformed))
+                {
+                    artist.AddProductionLocation(data.bio.placeformed);
+                }
             }
 
             artist.PremiereDate = yearFormed > 0 ? new DateTime(yearFormed, 1,1) : DateTime.MinValue;
@@ -52,7 +55,10 @@ namespace MediaBrowser.Controller.Providers.Music
         {
             foreach (var tag in tags.tag)
             {
-                item.AddGenre(tag.name);
+                if (!string.IsNullOrEmpty(tag.name))
+                {
+                    item.AddGenre(tag.name);
+                }
             }
         }
     }

+ 3 - 3
MediaBrowser.Controller/Providers/TV/FanArtTVProvider.cs

@@ -100,7 +100,7 @@ namespace MediaBrowser.Controller.Providers.TV
                             Logger.Debug("FanArtProvider getting ClearLogo for " + series.Name);
                             try
                             {
-                                series.SetImage(ImageType.Logo, await _providerManager.DownloadAndSaveImage(series, path, LOGO_FILE, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                                series.SetImage(ImageType.Logo, await _providerManager.DownloadAndSaveImage(series, path, LOGO_FILE, ConfigurationManager.Configuration.SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
                             }
                             catch (HttpException)
                             {
@@ -124,7 +124,7 @@ namespace MediaBrowser.Controller.Providers.TV
                             Logger.Debug("FanArtProvider getting ClearArt for " + series.Name);
                             try
                             {
-                                series.SetImage(ImageType.Art, await _providerManager.DownloadAndSaveImage(series, path, ART_FILE, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                                series.SetImage(ImageType.Art, await _providerManager.DownloadAndSaveImage(series, path, ART_FILE, ConfigurationManager.Configuration.SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
                             }
                             catch (HttpException)
                             {
@@ -148,7 +148,7 @@ namespace MediaBrowser.Controller.Providers.TV
                             Logger.Debug("FanArtProvider getting ThumbArt for " + series.Name);
                             try
                             {
-                                series.SetImage(ImageType.Disc, await _providerManager.DownloadAndSaveImage(series, path, THUMB_FILE, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                                series.SetImage(ImageType.Disc, await _providerManager.DownloadAndSaveImage(series, path, THUMB_FILE, ConfigurationManager.Configuration.SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
                             }
                             catch (HttpException)
                             {

+ 1 - 1
MediaBrowser.Controller/Providers/TV/RemoteEpisodeProvider.cs

@@ -233,7 +233,7 @@ namespace MediaBrowser.Controller.Providers.TV
 
                         try
                         {
-                            episode.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(episode, TVUtils.BannerUrl + p, Path.GetFileName(p), RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken);
+                            episode.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(episode, TVUtils.BannerUrl + p, Path.GetFileName(p), ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken);
                         }
                         catch (HttpException)
                         {

+ 4 - 4
MediaBrowser.Controller/Providers/TV/RemoteSeasonProvider.cs

@@ -177,7 +177,7 @@ namespace MediaBrowser.Controller.Providers.TV
                                 try
                                 {
                                     if (n != null)
-                                        season.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(season, TVUtils.BannerUrl + n.InnerText, "folder" + Path.GetExtension(n.InnerText), RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken).ConfigureAwait(false);
+                                        season.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(season, TVUtils.BannerUrl + n.InnerText, "folder" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken).ConfigureAwait(false);
                                 }
                                 catch (HttpException)
                                 {
@@ -204,7 +204,7 @@ namespace MediaBrowser.Controller.Providers.TV
                                                                                              TVUtils.BannerUrl + n.InnerText,
                                                                                              "banner" +
                                                                                              Path.GetExtension(n.InnerText),
-                                                                                             RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken).
+                                                                                             ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken).
                                                                ConfigureAwait(false);
 
                                         season.SetImage(ImageType.Banner, bannerImagePath);
@@ -231,7 +231,7 @@ namespace MediaBrowser.Controller.Providers.TV
                                     try
                                     {
                                         if (season.BackdropImagePaths == null) season.BackdropImagePaths = new List<string>();
-                                        season.BackdropImagePaths.Add(await _providerManager.DownloadAndSaveImage(season, TVUtils.BannerUrl + n.InnerText, "backdrop" + Path.GetExtension(n.InnerText), RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken).ConfigureAwait(false));
+                                        season.BackdropImagePaths.Add(await _providerManager.DownloadAndSaveImage(season, TVUtils.BannerUrl + n.InnerText, "backdrop" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken).ConfigureAwait(false));
                                     }
                                     catch (HttpException)
                                     {
@@ -265,7 +265,7 @@ namespace MediaBrowser.Controller.Providers.TV
                                                                                                  "backdrop" +
                                                                                                  Path.GetExtension(
                                                                                                      n.InnerText),
-                                                                                                 RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken)
+                                                                                                 ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken)
                                                                   .ConfigureAwait(false));
                                         }
                                         catch (HttpException)

+ 4 - 4
MediaBrowser.Controller/Providers/TV/RemoteSeriesProvider.cs

@@ -228,7 +228,7 @@ namespace MediaBrowser.Controller.Providers.TV
                     string n = doc.SafeGetString("//banner");
                     if (!string.IsNullOrWhiteSpace(n))
                     {
-                        series.SetImage(ImageType.Banner, await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n, "banner" + Path.GetExtension(n), TvDbResourcePool, cancellationToken).ConfigureAwait(false));
+                        series.SetImage(ImageType.Banner, await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n, "banner" + Path.GetExtension(n), ConfigurationManager.Configuration.SaveLocalMeta, TvDbResourcePool, cancellationToken).ConfigureAwait(false));
                     }
 
                     string s = doc.SafeGetString("//Network");
@@ -369,7 +369,7 @@ namespace MediaBrowser.Controller.Providers.TV
                             {
                                 try
                                 {
-                                    series.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n.InnerText, "folder" + Path.GetExtension(n.InnerText), TvDbResourcePool, cancellationToken).ConfigureAwait(false);
+                                    series.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n.InnerText, "folder" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, TvDbResourcePool, cancellationToken).ConfigureAwait(false);
                                 }
                                 catch (HttpException)
                                 {
@@ -392,7 +392,7 @@ namespace MediaBrowser.Controller.Providers.TV
                             {
                                 try
                                 {
-                                    var bannerImagePath = await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n.InnerText, "banner" + Path.GetExtension(n.InnerText), TvDbResourcePool, cancellationToken);
+                                    var bannerImagePath = await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n.InnerText, "banner" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, TvDbResourcePool, cancellationToken);
 
                                     series.SetImage(ImageType.Banner, bannerImagePath);
                                 }
@@ -421,7 +421,7 @@ namespace MediaBrowser.Controller.Providers.TV
                                 {
                                     try
                                     {
-                                        series.BackdropImagePaths.Add(await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + p.InnerText, bdName + Path.GetExtension(p.InnerText), TvDbResourcePool, cancellationToken).ConfigureAwait(false));
+                                        series.BackdropImagePaths.Add(await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + p.InnerText, bdName + Path.GetExtension(p.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, TvDbResourcePool, cancellationToken).ConfigureAwait(false));
                                     }
                                     catch (HttpException)
                                     {

+ 4 - 0
MediaBrowser.Controller/packages.config

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="morelinq" version="1.0.15631-beta" targetFramework="net45" />
+</packages>

+ 102 - 6
MediaBrowser.Server.Implementations/Library/LibraryManager.cs

@@ -1,5 +1,6 @@
 using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Progress;
 using MediaBrowser.Common.ScheduledTasks;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
@@ -103,7 +104,7 @@ namespace MediaBrowser.Server.Implementations.Library
         private readonly IUserManager _userManager;
 
         private readonly IUserDataRepository _userDataRepository;
-        
+
         /// <summary>
         /// Gets or sets the configuration manager.
         /// </summary>
@@ -244,7 +245,7 @@ namespace MediaBrowser.Server.Implementations.Library
             {
                 // Any number of configuration settings could change the way the library is refreshed, so do that now
                 _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
-                
+
                 if (refreshPeopleAfterUpdate)
                 {
                     _taskManager.CancelIfRunningAndQueue<PeopleValidationTask>();
@@ -285,7 +286,7 @@ namespace MediaBrowser.Server.Implementations.Library
 
             items.AddRange(userFolders);
 
-            return new ConcurrentDictionary<Guid,BaseItem>(items.ToDictionary(i => i.Id));
+            return new ConcurrentDictionary<Guid, BaseItem>(items.ToDictionary(i => i.Id));
         }
 
         /// <summary>
@@ -505,7 +506,7 @@ namespace MediaBrowser.Server.Implementations.Library
         {
             return _userRootFolders.GetOrAdd(userRootPath, key => RetrieveItem(userRootPath.GetMBId(typeof(UserRootFolder))) as UserRootFolder ?? (UserRootFolder)ResolvePath(userRootPath));
         }
-        
+
         /// <summary>
         /// Gets a Person
         /// </summary>
@@ -560,7 +561,20 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <returns>Task{Genre}.</returns>
         public Task<Artist> GetArtist(string name, bool allowSlowProviders = false)
         {
-            return GetImagesByNameItem<Artist>(ConfigurationManager.ApplicationPaths.ArtistsPath, name, CancellationToken.None, allowSlowProviders);
+            return GetArtist(name, CancellationToken.None, allowSlowProviders);
+        }
+
+        /// <summary>
+        /// Gets the artist.
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+        /// <param name="forceCreation">if set to <c>true</c> [force creation].</param>
+        /// <returns>Task{Artist}.</returns>
+        private Task<Artist> GetArtist(string name, CancellationToken cancellationToken, bool allowSlowProviders = false, bool forceCreation = false)
+        {
+            return GetImagesByNameItem<Artist>(ConfigurationManager.ApplicationPaths.ArtistsPath, name, cancellationToken, allowSlowProviders, forceCreation);
         }
 
         /// <summary>
@@ -764,6 +778,76 @@ namespace MediaBrowser.Server.Implementations.Library
             _logger.Info("People validation complete");
         }
 
+        public async Task ValidateArtists(CancellationToken cancellationToken, IProgress<double> progress)
+        {
+            const int maxTasks = 25;
+
+            var tasks = new List<Task>();
+
+            var artists = RootFolder.RecursiveChildren
+                .OfType<Audio>()
+                .SelectMany(c =>
+                {
+                    var list = c.Artists.ToList();
+
+                    if (!string.IsNullOrEmpty(c.AlbumArtist))
+                    {
+                        list.Add(c.AlbumArtist);
+                    }
+
+                    return list;
+                })
+                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .ToList();
+
+            var numComplete = 0;
+
+            foreach (var artist in artists)
+            {
+                if (tasks.Count > maxTasks)
+                {
+                    await Task.WhenAll(tasks).ConfigureAwait(false);
+                    tasks.Clear();
+
+                    // Safe cancellation point, when there are no pending tasks
+                    cancellationToken.ThrowIfCancellationRequested();
+                }
+
+                // Avoid accessing the foreach variable within the closure
+                var currentArtist = artist;
+
+                tasks.Add(Task.Run(async () =>
+                {
+                    cancellationToken.ThrowIfCancellationRequested();
+
+                    try
+                    {
+                        await GetArtist(currentArtist, cancellationToken, true, true).ConfigureAwait(false);
+                    }
+                    catch (IOException ex)
+                    {
+                        _logger.ErrorException("Error validating Artist {0}", ex, currentArtist);
+                    }
+
+                    // Update progress
+                    lock (progress)
+                    {
+                        numComplete++;
+                        double percent = numComplete;
+                        percent /= artists.Count;
+
+                        progress.Report(100 * percent);
+                    }
+                }));
+            }
+
+            await Task.WhenAll(tasks).ConfigureAwait(false);
+
+            progress.Report(100);
+
+            _logger.Info("Artist validation complete");
+        }
+
         /// <summary>
         /// Reloads the root media folder
         /// </summary>
@@ -796,8 +880,20 @@ namespace MediaBrowser.Server.Implementations.Library
                 await ValidateCollectionFolders(folder, cancellationToken).ConfigureAwait(false);
             }
 
+            var innerProgress = new ActionableProgress<double>();
+
+            innerProgress.RegisterAction(pct => progress.Report(pct * .8));
+
             // Now validate the entire media library
-            await RootFolder.ValidateChildren(progress, cancellationToken, recursive: true).ConfigureAwait(false);
+            await RootFolder.ValidateChildren(innerProgress, cancellationToken, recursive: true).ConfigureAwait(false);
+
+            innerProgress = new ActionableProgress<double>();
+
+            innerProgress.RegisterAction(pct => progress.Report(80 + pct * .2));
+
+            await ValidateArtists(cancellationToken, innerProgress);
+
+            progress.Report(100);
         }
 
         /// <summary>

+ 1 - 0
MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj

@@ -145,6 +145,7 @@
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Providers\ProviderManager.cs" />
     <Compile Include="Reflection\TypeMapper.cs" />
+    <Compile Include="ScheduledTasks\ArtistValidationTask.cs" />
     <Compile Include="ScheduledTasks\PeopleValidationTask.cs" />
     <Compile Include="ScheduledTasks\ChapterImagesTask.cs" />
     <Compile Include="ScheduledTasks\ImageCleanupTask.cs" />

+ 4 - 3
MediaBrowser.Server.Implementations/Providers/ProviderManager.cs

@@ -330,11 +330,12 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// <param name="item">The item.</param>
         /// <param name="source">The source.</param>
         /// <param name="targetName">Name of the target.</param>
+        /// <param name="saveLocally">if set to <c>true</c> [save locally].</param>
         /// <param name="resourcePool">The resource pool.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{System.String}.</returns>
         /// <exception cref="System.ArgumentNullException">item</exception>
-        public async Task<string> DownloadAndSaveImage(BaseItem item, string source, string targetName, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
+        public async Task<string> DownloadAndSaveImage(BaseItem item, string source, string targetName, bool saveLocally, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
         {
             if (item == null)
             {
@@ -354,13 +355,13 @@ namespace MediaBrowser.Server.Implementations.Providers
             }
 
             //download and save locally
-            var localPath = (ConfigurationManager.Configuration.SaveLocalMeta && item.MetaLocation != null) ?
+            var localPath = (saveLocally && item.MetaLocation != null) ?
                 Path.Combine(item.MetaLocation, targetName) :
                 _remoteImageCache.GetResourcePath(item.GetType().FullName + item.Path.ToLower(), targetName);
 
             var img = await _httpClient.Get(source, resourcePool, cancellationToken).ConfigureAwait(false);
 
-            if (ConfigurationManager.Configuration.SaveLocalMeta) // queue to media directories
+            if (saveLocally) // queue to media directories
             {
                 await SaveToLibraryFilesystem(item, localPath, img, cancellationToken).ConfigureAwait(false);
             }

+ 81 - 0
MediaBrowser.Server.Implementations/ScheduledTasks/ArtistValidationTask.cs

@@ -0,0 +1,81 @@
+using MediaBrowser.Common.ScheduledTasks;
+using MediaBrowser.Controller.Library;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.ScheduledTasks
+{
+    public class ArtistValidationTask
+    {
+        /// <summary>
+        /// The _library manager
+        /// </summary>
+        private readonly ILibraryManager _libraryManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PeopleValidationTask" /> class.
+        /// </summary>
+        /// <param name="libraryManager">The library manager.</param>
+        public ArtistValidationTask(ILibraryManager libraryManager)
+        {
+            _libraryManager = libraryManager;
+        }
+
+        /// <summary>
+        /// Creates the triggers that define when the task will run
+        /// </summary>
+        /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+        public IEnumerable<ITaskTrigger> GetDefaultTriggers()
+        {
+            return new ITaskTrigger[]
+                {
+                    new DailyTrigger { TimeOfDay = TimeSpan.FromHours(5) },
+
+                    new IntervalTrigger{ Interval = TimeSpan.FromHours(12)}
+                };
+        }
+
+        /// <summary>
+        /// Returns the task to be executed
+        /// </summary>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="progress">The progress.</param>
+        /// <returns>Task.</returns>
+        public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+        {
+            return _libraryManager.ValidateArtists(cancellationToken, progress);
+        }
+
+        /// <summary>
+        /// Gets the name of the task
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name
+        {
+            get { return "Refresh music artists"; }
+        }
+
+        /// <summary>
+        /// Gets the description.
+        /// </summary>
+        /// <value>The description.</value>
+        public string Description
+        {
+            get { return "Updates metadata for music artists in your media library."; }
+        }
+
+        /// <summary>
+        /// Gets the category.
+        /// </summary>
+        /// <value>The category.</value>
+        public string Category
+        {
+            get
+            {
+                return "Library";
+            }
+        }
+    }
+}

+ 1 - 1
MediaBrowser.Server.Implementations/ScheduledTasks/PeopleValidationTask.cs

@@ -66,7 +66,7 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks
         /// <value>The description.</value>
         public string Description
         {
-            get { return "Updates metadata for actors, artists and directors in your media library."; }
+            get { return "Updates metadata for actors and directors in your media library."; }
         }
 
         /// <summary>

+ 3 - 1
MediaBrowser.Server.Implementations/Sqlite/SQLiteRepository.cs

@@ -150,6 +150,8 @@ namespace MediaBrowser.Server.Implementations.Sqlite
             GC.SuppressFinalize(this);
         }
 
+        private readonly object _disposeLock = new object();
+
         /// <summary>
         /// Releases unmanaged and - optionally - managed resources.
         /// </summary>
@@ -160,7 +162,7 @@ namespace MediaBrowser.Server.Implementations.Sqlite
             {
                 try
                 {
-                    lock (this)
+                    lock (_disposeLock)
                     {
                         if (connection != null)
                         {

+ 2 - 0
MediaBrowser.WebDashboard/Api/DashboardService.cs

@@ -477,6 +477,8 @@ namespace MediaBrowser.WebDashboard.Api
                                       "moviesrecommended.js",
                                       "moviestudios.js",
                                       "movietrailers.js",
+                                      "musicalbums.js",
+                                      "musicartists.js",
                                       "musicgenres.js",
                                       "playlist.js",
                                       "plugincatalogpage.js",

+ 321 - 43
MediaBrowser.WebDashboard/ApiClient.js

@@ -862,13 +862,19 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
         /**
          * Gets a studio
          */
-        self.getStudio = function (name) {
+        self.getStudio = function (name, userId) {
 
             if (!name) {
                 throw new Error("null name");
             }
 
-            var url = self.getUrl("Studios/" + encodeName(name));
+            var options = {};
+
+            if (userId) {
+                options.userId = userId;
+            }
+
+            var url = self.getUrl("Studios/" + encodeName(name), options);
 
             return self.ajax({
                 type: "GET",
@@ -880,13 +886,43 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
         /**
          * Gets a genre
          */
-        self.getGenre = function (name) {
+        self.getGenre = function (name, userId) {
 
             if (!name) {
                 throw new Error("null name");
             }
 
-            var url = self.getUrl("Genres/" + encodeName(name));
+            var options = {};
+
+            if (userId) {
+                options.userId = userId;
+            }
+
+            var url = self.getUrl("Genres/" + encodeName(name), options);
+
+            return self.ajax({
+                type: "GET",
+                url: url,
+                dataType: "json"
+            });
+        };
+
+        /**
+         * Gets an artist
+         */
+        self.getArtist = function (name, userId) {
+
+            if (!name) {
+                throw new Error("null name");
+            }
+
+            var options = {};
+
+            if (userId) {
+                options.userId = userId;
+            }
+
+            var url = self.getUrl("Artists/" + encodeName(name), options);
 
             return self.ajax({
                 type: "GET",
@@ -898,13 +934,19 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
         /**
          * Gets a year
          */
-        self.getYear = function (year) {
+        self.getYear = function (yea, userId) {
 
-            if (!year) {
-                throw new Error("null year");
+            if (!name) {
+                throw new Error("null name");
+            }
+
+            var options = {};
+
+            if (userId) {
+                options.userId = userId;
             }
 
-            var url = self.getUrl("Years/" + year);
+            var url = self.getUrl("Years/" + encodeName(name), options);
 
             return self.ajax({
                 type: "GET",
@@ -916,13 +958,19 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
         /**
          * Gets a Person
          */
-        self.getPerson = function (name) {
+        self.getPerson = function (name, userId) {
 
             if (!name) {
                 throw new Error("null name");
             }
 
-            var url = self.getUrl("Persons/" + encodeName(name));
+            var options = {};
+
+            if (userId) {
+                options.userId = userId;
+            }
+
+            var url = self.getUrl("Persons/" + encodeName(name), options);
 
             return self.ajax({
                 type: "GET",
@@ -1131,6 +1179,41 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
             return self.getUrl(url, options);
         };
 
+        /**
+         * Constructs a url for a artist image
+         * @param {String} name
+         * @param {Object} options
+         * Options supports the following properties:
+         * width - download the image at a fixed width
+         * height - download the image at a fixed height
+         * maxWidth - download the image at a maxWidth
+         * maxHeight - download the image at a maxHeight
+         * quality - A scale of 0-100. This should almost always be omitted as the default will suffice.
+         * For best results do not specify both width and height together, as aspect ratio might be altered.
+         */
+        self.getArtistImageUrl = function (name, options) {
+
+            if (!name) {
+                throw new Error("null name");
+            }
+
+            options = options || {
+
+            };
+
+            var url = "Artists/" + encodeName(name) + "/Images/" + options.type;
+
+            if (options.index != null) {
+                url += "/" + options.index;
+            }
+
+            // Don't put these on the query string
+            delete options.type;
+            delete options.index;
+
+            return self.getUrl(url, options);
+        };
+
         /**
          * Constructs a url for a studio image
          * @param {String} name
@@ -1524,6 +1607,27 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
             });
         };
 
+        /**
+            Gets artists from an item
+        */
+        self.getArtists = function (userId, options) {
+
+            if (!userId) {
+                throw new Error("null userId");
+            }
+
+            options = options || {};
+            options.userId = userId;
+
+            var url = self.getUrl("Artists", options);
+
+            return self.ajax({
+                type: "GET",
+                url: url,
+                dataType: "json"
+            });
+        };
+
         /**
             Gets genres from an item
         */
@@ -1533,12 +1637,10 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
                 throw new Error("null userId");
             }
 
-            var parentId = options.parentId || "root";
-
-            // Don't put these on the query string
-            delete options.parentId;
+            options = options || {};
+            options.userId = userId;
 
-            var url = self.getUrl("Users/" + userId + "/Items/" + parentId + "/Genres", options);
+            var url = self.getUrl("Genres", options);
 
             return self.ajax({
                 type: "GET",
@@ -1556,12 +1658,10 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
                 throw new Error("null userId");
             }
 
-            var parentId = options.parentId || "root";
-
-            // Don't put these on the query string
-            delete options.parentId;
+            options = options || {};
+            options.userId = userId;
 
-            var url = self.getUrl("Users/" + userId + "/Items/" + parentId + "/Persons", options);
+            var url = self.getUrl("Persons", options);
 
             return self.ajax({
                 type: "GET",
@@ -1579,12 +1679,10 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
                 throw new Error("null userId");
             }
 
-            var parentId = options.parentId || "root";
-
-            // Don't put these on the query string
-            delete options.parentId;
+            options = options || {};
+            options.userId = userId;
 
-            var url = self.getUrl("Users/" + userId + "/Items/" + parentId + "/Studios", options);
+            var url = self.getUrl("Studios", options);
 
             return self.ajax({
                 type: "GET",
@@ -1722,7 +1820,67 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
          * @param {String} name
          * @param {Boolean} isFavorite
          */
-        self.updateItemByNameFavoriteStatus = function (userId, name, isFavorite) {
+        self.updateFavoriteArtistStatus = function (userId, name, isFavorite) {
+
+            if (!userId) {
+                throw new Error("null userId");
+            }
+
+            if (!name) {
+                throw new Error("null name");
+            }
+
+            var url = self.getUrl("Users/" + userId + "/Favorites/Artists/" + encodeName(name));
+
+            var method = isFavorite ? "POST" : "DELETE";
+
+            return self.ajax({
+                type: method,
+                url: url
+            });
+        };
+
+        self.updateFavoritePersonStatus = function (userId, name, isFavorite) {
+
+            if (!userId) {
+                throw new Error("null userId");
+            }
+
+            if (!name) {
+                throw new Error("null name");
+            }
+
+            var url = self.getUrl("Users/" + userId + "/Favorites/Persons/" + encodeName(name));
+
+            var method = isFavorite ? "POST" : "DELETE";
+
+            return self.ajax({
+                type: method,
+                url: url
+            });
+        };
+
+        self.updateFavoriteStudioStatus = function (userId, name, isFavorite) {
+
+            if (!userId) {
+                throw new Error("null userId");
+            }
+
+            if (!name) {
+                throw new Error("null name");
+            }
+
+            var url = self.getUrl("Users/" + userId + "/Favorites/Studios/" + encodeName(name));
+
+            var method = isFavorite ? "POST" : "DELETE";
+
+            return self.ajax({
+                type: method,
+                url: url
+            });
+        };
+
+        self.updateFavoriteGenreStatus = function (userId, name, isFavorite) {
 
             if (!userId) {
                 throw new Error("null userId");
@@ -1732,7 +1890,7 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
                 throw new Error("null name");
             }
 
-            var url = self.getUrl("Users/" + userId + "/ItemsByName/Favorites/" + encodeName(name));
+            var url = self.getUrl("Users/" + userId + "/Favorites/Genres/" + encodeName(name));
 
             var method = isFavorite ? "POST" : "DELETE";
 
@@ -1748,7 +1906,27 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
         * @param {String} name
         * @param {Boolean} likes
         */
-        self.updateItemByNameRating = function (userId, name, likes) {
+        self.updateArtistRating = function (userId, name, likes) {
+
+            if (!userId) {
+                throw new Error("null userId");
+            }
+
+            if (!name) {
+                throw new Error("null name");
+            }
+
+            var url = self.getUrl("Users/" + userId + "/Ratings/Artists/" + encodeName(name), {
+                likes: likes
+            });
+
+            return self.ajax({
+                type: "POST",
+                url: url
+            });
+        };
+
+        self.updatePersonRating = function (userId, name, likes) {
 
             if (!userId) {
                 throw new Error("null userId");
@@ -1758,7 +1936,47 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
                 throw new Error("null name");
             }
 
-            var url = self.getUrl("Users/" + userId + "/ItemsByName/" + encodeName(name) + "/Rating", {
+            var url = self.getUrl("Users/" + userId + "/Ratings/Persons/" + encodeName(name), {
+                likes: likes
+            });
+
+            return self.ajax({
+                type: "POST",
+                url: url
+            });
+        };
+
+        self.updateStudioRating = function (userId, name, likes) {
+
+            if (!userId) {
+                throw new Error("null userId");
+            }
+
+            if (!name) {
+                throw new Error("null name");
+            }
+
+            var url = self.getUrl("Users/" + userId + "/Ratings/Studios/" + encodeName(name), {
+                likes: likes
+            });
+
+            return self.ajax({
+                type: "POST",
+                url: url
+            });
+        };
+
+        self.updateGenreRating = function (userId, name, likes) {
+
+            if (!userId) {
+                throw new Error("null userId");
+            }
+
+            if (!name) {
+                throw new Error("null name");
+            }
+
+            var url = self.getUrl("Users/" + userId + "/Ratings/Genres/" + encodeName(name), {
                 likes: likes
             });
 
@@ -1773,7 +1991,7 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
         * @param {String} userId
         * @param {String} name
         */
-        self.clearItemByNameRating = function (userId, name) {
+        self.clearArtistRating = function (userId, name) {
 
             if (!userId) {
                 throw new Error("null userId");
@@ -1783,7 +2001,61 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
                 throw new Error("null name");
             }
 
-            var url = self.getUrl("Users/" + userId + "/ItemsByName/" + encodeName(name) + "/Rating");
+            var url = self.getUrl("Users/" + userId + "/Ratings/Artists/" + encodeName(name));
+
+            return self.ajax({
+                type: "DELETE",
+                url: url
+            });
+        };
+
+        self.clearPersonRating = function (userId, name) {
+
+            if (!userId) {
+                throw new Error("null userId");
+            }
+
+            if (!name) {
+                throw new Error("null name");
+            }
+
+            var url = self.getUrl("Users/" + userId + "/Ratings/Persons/" + encodeName(name));
+
+            return self.ajax({
+                type: "DELETE",
+                url: url
+            });
+        };
+
+        self.clearStudioRating = function (userId, name) {
+
+            if (!userId) {
+                throw new Error("null userId");
+            }
+
+            if (!name) {
+                throw new Error("null name");
+            }
+
+            var url = self.getUrl("Users/" + userId + "/Ratings/Studios/" + encodeName(name));
+
+            return self.ajax({
+                type: "DELETE",
+                url: url
+            });
+        };
+
+        self.clearGenreRating = function (userId, name) {
+
+            if (!userId) {
+                throw new Error("null userId");
+            }
+
+            if (!name) {
+                throw new Error("null name");
+            }
+
+            var url = self.getUrl("Users/" + userId + "/Ratings/Genres/" + encodeName(name));
 
             return self.ajax({
                 type: "DELETE",
@@ -1792,11 +2064,9 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
         };
 
         /**
-        * Gets the full user data object for an item by name.
-        * @param {String} userId
-        * @param {String} name
+            Gets a variety of item counts that a person appears in
         */
-        self.getItembyNameUserData = function (userId, name) {
+        self.getPersonItemCounts = function (userId, name) {
 
             if (!userId) {
                 throw new Error("null userId");
@@ -1806,7 +2076,9 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
                 throw new Error("null name");
             }
 
-            var url = self.getUrl("Users/" + userId + "/ItemsByName/" + encodeName(name) + "/UserData");
+            var url = self.getUrl("Persons/" + encodeName(name) + "/Counts", {
+                userId: userId
+            });
 
             return self.ajax({
                 type: "GET",
@@ -1816,9 +2088,9 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
         };
 
         /**
-            Gets a variety of item counts that a person appears in
+            Gets a variety of item counts that a genre appears in
         */
-        self.getPersonItemCounts = function (userId, name) {
+        self.getGenreItemCounts = function (userId, name) {
 
             if (!userId) {
                 throw new Error("null userId");
@@ -1828,7 +2100,9 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
                 throw new Error("null name");
             }
 
-            var url = self.getUrl("Users/" + userId + "/Persons/" + encodeName(name) + "/Counts");
+            var url = self.getUrl("Genres/" + encodeName(name) + "/Counts", {
+                userId: userId
+            });
 
             return self.ajax({
                 type: "GET",
@@ -1838,9 +2112,9 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
         };
 
         /**
-            Gets a variety of item counts that a genre appears in
+            Gets a variety of item counts that an artist appears in
         */
-        self.getGenreItemCounts = function (userId, name) {
+        self.getArtistItemCounts = function (userId, name) {
 
             if (!userId) {
                 throw new Error("null userId");
@@ -1850,7 +2124,9 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
                 throw new Error("null name");
             }
 
-            var url = self.getUrl("Users/" + userId + "/Genres/" + encodeName(name) + "/Counts");
+            var url = self.getUrl("Artists/" + encodeName(name) + "/Counts", {
+                userId: userId
+            });
 
             return self.ajax({
                 type: "GET",
@@ -1872,7 +2148,9 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
                 throw new Error("null name");
             }
 
-            var url = self.getUrl("Users/" + userId + "/Studios/" + encodeName(name) + "/Counts");
+            var url = self.getUrl("Studios/" + encodeName(name) + "/Counts", {
+                userId: userId
+            });
 
             return self.ajax({
                 type: "GET",

+ 12 - 0
MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

@@ -237,6 +237,12 @@
     <Content Include="dashboard-ui\movietrailers.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="dashboard-ui\musicalbums.html">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+    <Content Include="dashboard-ui\musicartists.html">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\musicgenres.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
@@ -288,6 +294,12 @@
     <Content Include="dashboard-ui\scripts\movietrailers.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="dashboard-ui\scripts\musicalbums.js">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+    <Content Include="dashboard-ui\scripts\musicartists.js">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\scripts\musicgenres.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>

+ 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.80" targetFramework="net45" />
+  <package id="MediaBrowser.ApiClient.Javascript" version="3.0.85" targetFramework="net45" />
   <package id="ServiceStack.Common" version="3.9.43" targetFramework="net45" />
   <package id="ServiceStack.Text" version="3.9.43" targetFramework="net45" />
 </packages>