Selaa lähdekoodia

de-normalize item by name data. create counts during library scan for fast access.

Luke Pulverenti 11 vuotta sitten
vanhempi
sitoutus
740a10a4e3
63 muutettua tiedostoa jossa 1923 lisäystä ja 971 poistoa
  1. 1 66
      MediaBrowser.Api/UserLibrary/ArtistsService.cs
  2. 5 20
      MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
  3. 1 41
      MediaBrowser.Api/UserLibrary/GameGenresService.cs
  4. 1 51
      MediaBrowser.Api/UserLibrary/GenresService.cs
  5. 2 21
      MediaBrowser.Api/UserLibrary/ItemsService.cs
  6. 1 45
      MediaBrowser.Api/UserLibrary/MusicGenresService.cs
  7. 1 71
      MediaBrowser.Api/UserLibrary/PersonsService.cs
  8. 1 58
      MediaBrowser.Api/UserLibrary/StudiosService.cs
  9. 1 1
      MediaBrowser.Api/UserLibrary/YearsService.cs
  10. 14 2
      MediaBrowser.Controller/Entities/Audio/Artist.cs
  11. 1 1
      MediaBrowser.Controller/Entities/Audio/Audio.cs
  12. 5 0
      MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs
  13. 9 0
      MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs
  14. 15 15
      MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
  15. 14 1
      MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
  16. 53 0
      MediaBrowser.Controller/Entities/BaseItem.cs
  17. 14 1
      MediaBrowser.Controller/Entities/GameGenre.cs
  18. 14 1
      MediaBrowser.Controller/Entities/Genre.cs
  19. 7 1
      MediaBrowser.Controller/Entities/IItemByName.cs
  20. 3 2
      MediaBrowser.Controller/Entities/MusicVideo.cs
  21. 14 1
      MediaBrowser.Controller/Entities/Person.cs
  22. 14 1
      MediaBrowser.Controller/Entities/Studio.cs
  23. 5 0
      MediaBrowser.Controller/Entities/Video.cs
  24. 14 1
      MediaBrowser.Controller/Entities/Year.cs
  25. 32 0
      MediaBrowser.Controller/Library/ILibraryManager.cs
  26. 2 0
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  27. 10 0
      MediaBrowser.Controller/Providers/IDynamicInfoProvider.cs
  28. 2 0
      MediaBrowser.Model/ApiClient/IApiClient.cs
  29. 42 2
      MediaBrowser.Model/Dto/BaseItemDto.cs
  30. 4 0
      MediaBrowser.Model/Dto/ItemByNameCounts.cs
  31. 3 71
      MediaBrowser.Providers/ImageFromMediaLocationProvider.cs
  32. 2 2
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  33. 0 161
      MediaBrowser.Providers/Music/ArtistsPostScanTask.cs
  34. 85 0
      MediaBrowser.Providers/Music/MusicAlbumDynamicInfoProvider.cs
  35. 48 8
      MediaBrowser.Server.Implementations/Dto/DtoService.cs
  36. 4 0
      MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs
  37. 129 90
      MediaBrowser.Server.Implementations/Library/LibraryManager.cs
  38. 2 2
      MediaBrowser.Server.Implementations/Library/LuceneSearchEngine.cs
  39. 38 0
      MediaBrowser.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs
  40. 285 0
      MediaBrowser.Server.Implementations/Library/Validators/ArtistsValidator.cs
  41. 155 0
      MediaBrowser.Server.Implementations/Library/Validators/CountHelpers.cs
  42. 45 0
      MediaBrowser.Server.Implementations/Library/Validators/GameGenresPostScanTask.cs
  43. 132 0
      MediaBrowser.Server.Implementations/Library/Validators/GameGenresValidator.cs
  44. 42 0
      MediaBrowser.Server.Implementations/Library/Validators/GenresPostScanTask.cs
  45. 135 0
      MediaBrowser.Server.Implementations/Library/Validators/GenresValidator.cs
  46. 45 0
      MediaBrowser.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs
  47. 135 0
      MediaBrowser.Server.Implementations/Library/Validators/MusicGenresValidator.cs
  48. 137 0
      MediaBrowser.Server.Implementations/Library/Validators/PeoplePostScanTask.cs
  49. 45 0
      MediaBrowser.Server.Implementations/Library/Validators/StudiosPostScanTask.cs
  50. 132 0
      MediaBrowser.Server.Implementations/Library/Validators/StudiosValidator.cs
  51. 12 1
      MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
  52. 5 1
      MediaBrowser.Server.Implementations/Providers/ProviderManager.cs
  53. 0 81
      MediaBrowser.Server.Implementations/ScheduledTasks/ArtistValidationTask.cs
  54. 1 5
      MediaBrowser.Server.Implementations/Session/SessionManager.cs
  55. 0 7
      MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj
  56. BIN
      MediaBrowser.ServerApplication/Resources/Images/mblogoblackfull.png
  57. BIN
      MediaBrowser.ServerApplication/Resources/Images/mblogowhitefull.png
  58. BIN
      MediaBrowser.ServerApplication/Resources/Images/starEmpty.png
  59. BIN
      MediaBrowser.ServerApplication/Resources/Images/starFull.png
  60. BIN
      MediaBrowser.ServerApplication/Resources/Images/starHalf.png
  61. 0 138
      MediaBrowser.WebDashboard/ApiClient.js
  62. 1 1
      MediaBrowser.WebDashboard/packages.config
  63. 3 0
      MediaBrowser.sln

+ 1 - 66
MediaBrowser.Api/UserLibrary/ArtistsService.cs

@@ -22,28 +22,6 @@ namespace MediaBrowser.Api.UserLibrary
     {
     }
 
-    /// <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>
@@ -114,49 +92,6 @@ namespace MediaBrowser.Api.UserLibrary
             return await DtoService.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 name = DeSlugArtistName(request.Name, LibraryManager);
-
-            var items = GetItems(request.UserId).Where(i =>
-            {
-                var song = i as Audio;
-
-                if (song != null)
-                {
-                    return song.HasArtist(name);
-                }
-
-                var musicVideo = i as MusicVideo;
-
-                if (musicVideo != null)
-                {
-                    return musicVideo.HasArtist(name);
-                }
-                
-                return false;
-
-            }).ToList();
-
-            var counts = new ItemByNameCounts
-            {
-                TotalCount = items.Count,
-
-                SongCount = items.OfType<Audio>().Count(),
-
-                AlbumCount = items.Select(i => i.Parent).OfType<MusicAlbum>().Distinct().Count(),
-
-                MusicVideoCount = items.OfType<MusicVideo>().Count(i => i.HasArtist(name))
-            };
-
-            return ToOptimizedResult(counts);
-        }
-
         /// <summary>
         /// Gets the specified request.
         /// </summary>
@@ -193,7 +128,7 @@ namespace MediaBrowser.Api.UserLibrary
                     return list;
                 })
                 .Distinct(StringComparer.OrdinalIgnoreCase)
-                .Select(name => new IbnStub<Artist>(name, () => itemsList.Where(i => i.HasArtist(name)), GetEntity));
+                .Select(name => new IbnStub<Artist>(name, GetEntity));
         }
 
         /// <summary>

+ 5 - 20
MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs

@@ -18,7 +18,7 @@ namespace MediaBrowser.Api.UserLibrary
     /// </summary>
     /// <typeparam name="TItemType">The type of the T item type.</typeparam>
     public abstract class BaseItemsByNameService<TItemType> : BaseApiService
-        where TItemType : BaseItem
+        where TItemType : BaseItem, IItemByName
     {
         /// <summary>
         /// The _user manager
@@ -38,6 +38,8 @@ namespace MediaBrowser.Api.UserLibrary
         /// <param name="userManager">The user manager.</param>
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="userDataRepository">The user data repository.</param>
+        /// <param name="itemRepository">The item repository.</param>
+        /// <param name="dtoService">The dto service.</param>
         protected BaseItemsByNameService(IUserManager userManager, ILibraryManager libraryManager, IUserDataRepository userDataRepository, IItemRepository itemRepository, IDtoService dtoService)
         {
             UserManager = userManager;
@@ -292,7 +294,7 @@ namespace MediaBrowser.Api.UserLibrary
         /// <returns>Task{DtoBaseItem}.</returns>
         private async Task<BaseItemDto> GetDto(IbnStub<TItemType> stub, User user, List<ItemFields> fields)
         {
-            BaseItem item;
+            TItemType item;
 
             try
             {
@@ -307,14 +309,6 @@ namespace MediaBrowser.Api.UserLibrary
             var dto = user == null ? await DtoService.GetBaseItemDto(item, fields).ConfigureAwait(false) :
                 await DtoService.GetBaseItemDto(item, fields, user).ConfigureAwait(false);
 
-            if (fields.Contains(ItemFields.ItemCounts))
-            {
-                var items = stub.Items;
-
-                dto.ChildCount = items.Count;
-                dto.RecentlyAddedItemCount = items.Count(i => i.IsRecentlyAdded());
-            }
-
             return dto;
         }
 
@@ -367,9 +361,6 @@ namespace MediaBrowser.Api.UserLibrary
     public class IbnStub<T>
         where T : BaseItem
     {
-        private readonly Func<IEnumerable<BaseItem>> _childItemsFunction;
-        private List<BaseItem> _childItems;
-
         private readonly Func<string,Task<T>> _itemFunction;
         private Task<T> _itemTask;
         
@@ -377,11 +368,6 @@ namespace MediaBrowser.Api.UserLibrary
 
         private UserItemData _userData;
 
-        public List<BaseItem> Items
-        {
-            get { return _childItems ?? (_childItems = _childItemsFunction().ToList()); }
-        }
-
         public Task<T> GetItem()
         {
             return _itemTask ?? (_itemTask = _itemFunction(Name));
@@ -394,10 +380,9 @@ namespace MediaBrowser.Api.UserLibrary
             return _userData ?? (_userData = repo.GetUserData(userId, item.GetUserDataKey()));
         }
 
-        public IbnStub(string name, Func<IEnumerable<BaseItem>> childItems, Func<string,Task<T>> item)
+        public IbnStub(string name, Func<string,Task<T>> item)
         {
             Name = name;
-            _childItemsFunction = childItems;
             _itemFunction = item;
         }
     }

+ 1 - 41
MediaBrowser.Api/UserLibrary/GameGenresService.cs

@@ -23,25 +23,6 @@ namespace MediaBrowser.Api.UserLibrary
         }
     }
 
-    [Route("/GameGenres/{Name}/Counts", "GET")]
-    [Api(Description = "Gets item counts of library items that a genre appears in")]
-    public class GetGameGenreItemCounts : IReturn<ItemByNameCounts>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [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.
-        /// </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; }
-    }
-
     [Route("/GameGenres/{Name}", "GET")]
     [Api(Description = "Gets a Game genre, by name")]
     public class GetGameGenre : IReturn<BaseItemDto>
@@ -127,7 +108,7 @@ namespace MediaBrowser.Api.UserLibrary
             return itemsList
                 .SelectMany(i => i.Genres)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
-                .Select(name => new IbnStub<GameGenre>(name, () => itemsList.Where(i => i.Genres.Contains(name, StringComparer.OrdinalIgnoreCase)), GetEntity));
+                .Select(name => new IbnStub<GameGenre>(name, GetEntity));
         }
 
         /// <summary>
@@ -139,26 +120,5 @@ namespace MediaBrowser.Api.UserLibrary
         {
             return LibraryManager.GetGameGenre(name);
         }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetGameGenreItemCounts request)
-        {
-            var name = DeSlugGameGenreName(request.Name, LibraryManager);
-
-            var items = GetItems(request.UserId).Where(i => i.Genres != null && i.Genres.Contains(name, StringComparer.OrdinalIgnoreCase)).ToList();
-
-            var counts = new ItemByNameCounts
-            {
-                TotalCount = items.Count,
-
-                GameCount = items.OfType<Game>().Count()
-            };
-
-            return ToOptimizedResult(counts);
-        }
     }
 }

+ 1 - 51
MediaBrowser.Api/UserLibrary/GenresService.cs

@@ -1,7 +1,5 @@
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
@@ -23,25 +21,6 @@ namespace MediaBrowser.Api.UserLibrary
     {
     }
 
-    [Route("/Genres/{Name}/Counts", "GET")]
-    [Api(Description = "Gets item counts of library items that a genre appears in")]
-    public class GetGenreItemCounts : IReturn<ItemByNameCounts>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [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.
-        /// </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 GetGenre
     /// </summary>
@@ -133,7 +112,7 @@ namespace MediaBrowser.Api.UserLibrary
             return itemsList
                 .SelectMany(i => i.Genres)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
-                .Select(name => new IbnStub<Genre>(name, () => itemsList.Where(i => i.Genres.Contains(name, StringComparer.OrdinalIgnoreCase)), GetEntity));
+                .Select(name => new IbnStub<Genre>(name, GetEntity));
         }
 
         /// <summary>
@@ -145,34 +124,5 @@ 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 name = DeSlugGenreName(request.Name, LibraryManager);
-
-            var items = GetItems(request.UserId).Where(i => i.Genres.Contains(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<Game>().Count(),
-
-                AdultVideoCount = items.OfType<AdultVideo>().Count()
-            };
-
-            return ToOptimizedResult(counts);
-        }
     }
 }

+ 2 - 21
MediaBrowser.Api/UserLibrary/ItemsService.cs

@@ -427,28 +427,9 @@ namespace MediaBrowser.Api.UserLibrary
 
                 items = items.Where(i =>
                 {
-                    var audio = i as Audio;
-
-                    if (audio != null)
-                    {
-                        return artists.Any(audio.HasArtist);
-                    }
-
-                    var album = i as MusicAlbum;
-
-                    if (album != null)
-                    {
-                        return artists.Any(album.HasArtist);
-                    }
+                    var audio = i as IHasArtist;
 
-                    var musicVideo = i as MusicVideo;
-
-                    if (musicVideo != null)
-                    {
-                        return artists.Any(musicVideo.HasArtist);
-                    }
-
-                    return false;
+                    return audio != null && artists.Any(audio.HasArtist);
                 });
             }
 

+ 1 - 45
MediaBrowser.Api/UserLibrary/MusicGenresService.cs

@@ -23,25 +23,6 @@ namespace MediaBrowser.Api.UserLibrary
         }
     }
 
-    [Route("/MusicGenres/{Name}/Counts", "GET")]
-    [Api(Description = "Gets item counts of library items that a genre appears in")]
-    public class GetMusicGenreItemCounts : IReturn<ItemByNameCounts>
-    {
-        /// <summary>
-        /// Gets or sets the user id.
-        /// </summary>
-        /// <value>The user id.</value>
-        [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.
-        /// </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; }
-    }
-
     [Route("/MusicGenres/{Name}", "GET")]
     [Api(Description = "Gets a music genre, by name")]
     public class GetMusicGenre : IReturn<BaseItemDto>
@@ -127,7 +108,7 @@ namespace MediaBrowser.Api.UserLibrary
             return itemsList
                 .SelectMany(i => i.Genres)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
-                .Select(name => new IbnStub<MusicGenre>(name, () => itemsList.Where(i => i.Genres.Contains(name, StringComparer.OrdinalIgnoreCase)), GetEntity));
+                .Select(name => new IbnStub<MusicGenre>(name, GetEntity));
         }
 
         /// <summary>
@@ -139,30 +120,5 @@ namespace MediaBrowser.Api.UserLibrary
         {
             return LibraryManager.GetMusicGenre(name);
         }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetMusicGenreItemCounts request)
-        {
-            var name = DeSlugGenreName(request.Name, LibraryManager);
-
-            var items = GetItems(request.UserId).Where(i => i.Genres != null && i.Genres.Contains(name, StringComparer.OrdinalIgnoreCase)).ToList();
-
-            var counts = new ItemByNameCounts
-            {
-                TotalCount = items.Count,
-
-                SongCount = items.OfType<Audio>().Count(),
-
-                AlbumCount = items.OfType<MusicAlbum>().Count(),
-
-                MusicVideoCount = items.OfType<MusicVideo>().Count()
-            };
-
-            return ToOptimizedResult(counts);
-        }
     }
 }

+ 1 - 71
MediaBrowser.Api/UserLibrary/PersonsService.cs

@@ -1,8 +1,5 @@
 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;
@@ -29,28 +26,6 @@ namespace MediaBrowser.Api.UserLibrary
         public string PersonTypes { get; set; }
     }
 
-    /// <summary>
-    /// Class GetPersonItemCounts
-    /// </summary>
-    [Route("/Persons/{Name}/Counts", "GET")]
-    [Api(Description = "Gets item counts of library items that a person appears in")]
-    public class GetPersonItemCounts : 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 person name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-    }
-
     /// <summary>
     /// Class GetPerson
     /// </summary>
@@ -136,43 +111,6 @@ namespace MediaBrowser.Api.UserLibrary
             return ToOptimizedResult(result);
         }
 
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetPersonItemCounts request)
-        {
-            var name = DeSlugPersonName(request.Name, LibraryManager);
-
-            var items = GetItems(request.UserId).Where(i => i.People != null && i.People.Any(p => string.Equals(p.Name, name, StringComparison.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<Game>().Count(),
-
-                SongCount = items.OfType<Audio>().Count(),
-
-                AlbumCount = items.OfType<MusicAlbum>().Count(),
-
-                EpisodeCount = items.OfType<Episode>().Count(),
-
-                MusicVideoCount = items.OfType<MusicVideo>().Count(),
-
-                AdultVideoCount = items.OfType<AdultVideo>().Count()
-            };
-
-            return ToOptimizedResult(counts);
-        }
-
         /// <summary>
         /// Gets all items.
         /// </summary>
@@ -193,15 +131,7 @@ namespace MediaBrowser.Api.UserLibrary
                 .Select(i => i.Name)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
 
-                .Select(name => new IbnStub<Person>(name, () =>
-                {
-                    if (personTypes.Length == 0)
-                    {
-                        return itemsList.Where(i => i.People.Any(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)));
-                    }
-
-                    return itemsList.Where(i => i.People.Any(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase) && (personTypes.Contains(p.Type ?? string.Empty, StringComparer.OrdinalIgnoreCase) || personTypes.Contains(p.Role ?? string.Empty, StringComparer.OrdinalIgnoreCase))));
-                }, GetEntity)
+                .Select(name => new IbnStub<Person>(name, GetEntity)
             );
         }
 

+ 1 - 58
MediaBrowser.Api/UserLibrary/StudiosService.cs

@@ -1,8 +1,5 @@
 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;
@@ -24,25 +21,6 @@ namespace MediaBrowser.Api.UserLibrary
     {
     }
 
-    [Route("/Studios/{Name}/Counts", "GET")]
-    [Api(Description = "Gets item counts of library items that a studio appears in")]
-    public class GetStudioItemCounts : 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 studio name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Name { get; set; }
-    }
-
     /// <summary>
     /// Class GetStudio
     /// </summary>
@@ -109,41 +87,6 @@ namespace MediaBrowser.Api.UserLibrary
             return await DtoService.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 name = DeSlugStudioName(request.Name, LibraryManager);
-
-            var items = GetItems(request.UserId).Where(i => i.Studios != null && i.Studios.Contains(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<Game>().Count(),
-
-                SongCount = items.OfType<Audio>().Count(),
-
-                AlbumCount = items.OfType<MusicAlbum>().Count(),
-
-                MusicVideoCount = items.OfType<MusicVideo>().Count(),
-
-                AdultVideoCount = items.OfType<AdultVideo>().Count()
-            };
-
-            return ToOptimizedResult(counts);
-        }
-
         /// <summary>
         /// Gets the specified request.
         /// </summary>
@@ -169,7 +112,7 @@ namespace MediaBrowser.Api.UserLibrary
             return itemsList
                 .SelectMany(i => i.Studios)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
-                .Select(name => new IbnStub<Studio>(name, () => itemsList.Where(i => i.Studios.Contains(name, StringComparer.OrdinalIgnoreCase)), GetEntity));
+                .Select(name => new IbnStub<Studio>(name, GetEntity));
         }
 
         /// <summary>

+ 1 - 1
MediaBrowser.Api/UserLibrary/YearsService.cs

@@ -118,7 +118,7 @@ namespace MediaBrowser.Api.UserLibrary
             return itemsList
                 .Select(i => i.ProductionYear.Value)
                 .Distinct()
-                .Select(year => new IbnStub<Year>(year.ToString(UsCulture), () => itemsList.Where(i => i.ProductionYear.HasValue && i.ProductionYear.Value == year), GetEntity));
+                .Select(year => new IbnStub<Year>(year.ToString(UsCulture), GetEntity));
         }
 
         /// <summary>

+ 14 - 2
MediaBrowser.Controller/Entities/Audio/Artist.cs

@@ -1,11 +1,20 @@
-
+using MediaBrowser.Model.Dto;
+using System;
+using System.Collections.Generic;
+
 namespace MediaBrowser.Controller.Entities.Audio
 {
     /// <summary>
     /// Class Artist
     /// </summary>
-    public class Artist : BaseItem, IItemByName
+    public class Artist : BaseItem, IItemByName, IHasMusicGenres
     {
+        public Artist()
+        {
+            ItemCounts = new ItemByNameCounts();
+            UserItemCounts = new Dictionary<Guid, ItemByNameCounts>();
+        }
+
         public string LastFmImageUrl { get; set; }
         
         /// <summary>
@@ -17,5 +26,8 @@ namespace MediaBrowser.Controller.Entities.Audio
             return "Artist-" + Name;
         }
 
+        public ItemByNameCounts ItemCounts { get; set; }
+
+        public Dictionary<Guid, ItemByNameCounts> UserItemCounts { get; set; }
     }
 }

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

@@ -9,7 +9,7 @@ namespace MediaBrowser.Controller.Entities.Audio
     /// <summary>
     /// Class Audio
     /// </summary>
-    public class Audio : BaseItem, IHasMediaStreams, IHasAlbumArtist
+    public class Audio : BaseItem, IHasMediaStreams, IHasAlbumArtist, IHasArtist, IHasMusicGenres
     {
         public Audio()
         {

+ 5 - 0
MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs

@@ -5,4 +5,9 @@ namespace MediaBrowser.Controller.Entities.Audio
     {
         string AlbumArtist { get; }
     }
+
+    public interface IHasArtist
+    {
+        bool HasArtist(string name);
+    }
 }

+ 9 - 0
MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs

@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Entities.Audio
+{
+    public interface IHasMusicGenres
+    {
+        List<string> Genres { get; }
+    }
+}

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

@@ -1,4 +1,5 @@
-using System.Linq;
+using System;
+using System.Linq;
 using System.Runtime.Serialization;
 
 namespace MediaBrowser.Controller.Entities.Audio
@@ -6,10 +7,15 @@ namespace MediaBrowser.Controller.Entities.Audio
     /// <summary>
     /// Class MusicAlbum
     /// </summary>
-    public class MusicAlbum : Folder, IHasAlbumArtist
+    public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres
     {
+        public MusicAlbum()
+        {
+            Artists = new string[] { };
+        }
+
         public string LastFmImageUrl { get; set; }
-        
+
         /// <summary>
         /// Songs will group into us so don't also include us in the index
         /// </summary>
@@ -60,23 +66,17 @@ namespace MediaBrowser.Controller.Entities.Audio
         /// <returns><c>true</c> if the specified artist has artist; otherwise, <c>false</c>.</returns>
         public bool HasArtist(string artist)
         {
-            return RecursiveChildren.OfType<Audio>().Any(i => i.HasArtist(artist));
+            return string.Equals(AlbumArtist, artist, StringComparison.OrdinalIgnoreCase)
+                   || Artists.Contains(artist, StringComparer.OrdinalIgnoreCase);
         }
 
-        public string AlbumArtist
-        {
-            get
-            {
-                return RecursiveChildren
-                  .OfType<Audio>()
-                  .Select(i => i.AlbumArtist)
-                  .FirstOrDefault(i => !string.IsNullOrEmpty(i));
-            }
-        }
+        public string AlbumArtist { get; set; }
+
+        public string[] Artists { get; set; }
     }
 
     public class MusicAlbumDisc : Folder
     {
-        
+
     }
 }

+ 14 - 1
MediaBrowser.Controller/Entities/Audio/MusicGenre.cs

@@ -1,4 +1,7 @@
-
+using MediaBrowser.Model.Dto;
+using System;
+using System.Collections.Generic;
+
 namespace MediaBrowser.Controller.Entities.Audio
 {
     /// <summary>
@@ -6,6 +9,12 @@ namespace MediaBrowser.Controller.Entities.Audio
     /// </summary>
     public class MusicGenre : BaseItem, IItemByName
     {
+        public MusicGenre()
+        {
+            ItemCounts = new ItemByNameCounts();
+            UserItemCounts = new Dictionary<Guid, ItemByNameCounts>();
+        }
+
         /// <summary>
         /// Gets the user data key.
         /// </summary>
@@ -14,5 +23,9 @@ namespace MediaBrowser.Controller.Entities.Audio
         {
             return "MusicGenre-" + Name;
         }
+
+        public ItemByNameCounts ItemCounts { get; set; }
+
+        public Dictionary<Guid, ItemByNameCounts> UserItemCounts { get; set; }
     }
 }

+ 53 - 0
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -1537,5 +1537,58 @@ namespace MediaBrowser.Controller.Entities
             // Refresh metadata
             return RefreshMetadata(CancellationToken.None, forceSave: true);
         }
+
+        /// <summary>
+        /// Validates that images within the item are still on the file system
+        /// </summary>
+        public void ValidateImages()
+        {
+            // Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below
+            var deletedKeys = Images
+                .ToList()
+                .Where(image => !File.Exists(image.Value))
+                .Select(i => i.Key)
+                .ToList();
+
+            // Now remove them from the dictionary
+            foreach (var key in deletedKeys)
+            {
+                Images.Remove(key);
+            }
+        }
+
+        /// <summary>
+        /// Validates that backdrops within the item are still on the file system
+        /// </summary>
+        public void ValidateBackdrops()
+        {
+            // Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below
+            var deletedImages = BackdropImagePaths
+                .Where(path => !File.Exists(path))
+                .ToList();
+
+            // Now remove them from the dictionary
+            foreach (var path in deletedImages)
+            {
+                BackdropImagePaths.Remove(path);
+            }
+        }
+
+        /// <summary>
+        /// Validates the screenshots.
+        /// </summary>
+        public void ValidateScreenshots()
+        {
+            // Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below
+            var deletedImages = ScreenshotImagePaths
+                .Where(path => !File.Exists(path))
+                .ToList();
+
+            // Now remove them from the dictionary
+            foreach (var path in deletedImages)
+            {
+                ScreenshotImagePaths.Remove(path);
+            }
+        }
     }
 }

+ 14 - 1
MediaBrowser.Controller/Entities/GameGenre.cs

@@ -1,8 +1,17 @@
-
+using MediaBrowser.Model.Dto;
+using System;
+using System.Collections.Generic;
+
 namespace MediaBrowser.Controller.Entities
 {
     public class GameGenre : BaseItem, IItemByName
     {
+        public GameGenre()
+        {
+            ItemCounts = new ItemByNameCounts();
+            UserItemCounts = new Dictionary<Guid, ItemByNameCounts>();
+        }
+
         /// <summary>
         /// Gets the user data key.
         /// </summary>
@@ -11,5 +20,9 @@ namespace MediaBrowser.Controller.Entities
         {
             return "GameGenre-" + Name;
         }
+
+        public ItemByNameCounts ItemCounts { get; set; }
+
+        public Dictionary<Guid, ItemByNameCounts> UserItemCounts { get; set; }
     }
 }

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

@@ -1,4 +1,7 @@
-
+using MediaBrowser.Model.Dto;
+using System;
+using System.Collections.Generic;
+
 namespace MediaBrowser.Controller.Entities
 {
     /// <summary>
@@ -6,6 +9,12 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public class Genre : BaseItem, IItemByName
     {
+        public Genre()
+        {
+            ItemCounts = new ItemByNameCounts();
+            UserItemCounts = new Dictionary<Guid, ItemByNameCounts>();
+        }
+
         /// <summary>
         /// Gets the user data key.
         /// </summary>
@@ -14,5 +23,9 @@ namespace MediaBrowser.Controller.Entities
         {
             return "Genre-" + Name;
         }
+
+        public ItemByNameCounts ItemCounts { get; set; }
+
+        public Dictionary<Guid, ItemByNameCounts> UserItemCounts { get; set; }
     }
 }

+ 7 - 1
MediaBrowser.Controller/Entities/IItemByName.cs

@@ -1,4 +1,7 @@
-
+using MediaBrowser.Model.Dto;
+using System;
+using System.Collections.Generic;
+
 namespace MediaBrowser.Controller.Entities
 {
     /// <summary>
@@ -6,5 +9,8 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public interface IItemByName
     {
+        ItemByNameCounts ItemCounts { get; set; }
+
+        Dictionary<Guid, ItemByNameCounts> UserItemCounts { get; set; }
     }
 }

+ 3 - 2
MediaBrowser.Controller/Entities/MusicVideo.cs

@@ -1,9 +1,10 @@
-using MediaBrowser.Model.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Model.Entities;
 using System;
 
 namespace MediaBrowser.Controller.Entities
 {
-    public class MusicVideo : Video
+    public class MusicVideo : Video, IHasArtist, IHasMusicGenres
     {
         /// <summary>
         /// Gets or sets the artist.

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

@@ -1,4 +1,7 @@
-
+using MediaBrowser.Model.Dto;
+using System;
+using System.Collections.Generic;
+
 namespace MediaBrowser.Controller.Entities
 {
     /// <summary>
@@ -6,6 +9,16 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public class Person : BaseItem, IItemByName
     {
+        public Person()
+        {
+            ItemCounts = new ItemByNameCounts();
+            UserItemCounts = new Dictionary<Guid, ItemByNameCounts>();
+        }
+
+        public ItemByNameCounts ItemCounts { get; set; }
+
+        public Dictionary<Guid, ItemByNameCounts> UserItemCounts { get; set; }
+        
         /// <summary>
         /// Gets the user data key.
         /// </summary>

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

@@ -1,4 +1,7 @@
-
+using MediaBrowser.Model.Dto;
+using System;
+using System.Collections.Generic;
+
 namespace MediaBrowser.Controller.Entities
 {
     /// <summary>
@@ -6,6 +9,12 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public class Studio : BaseItem, IItemByName
     {
+        public Studio()
+        {
+            ItemCounts = new ItemByNameCounts();
+            UserItemCounts = new Dictionary<Guid, ItemByNameCounts>();
+        }
+
         /// <summary>
         /// Gets the user data key.
         /// </summary>
@@ -14,5 +23,9 @@ namespace MediaBrowser.Controller.Entities
         {
             return "Studio-" + Name;
         }
+
+        public ItemByNameCounts ItemCounts { get; set; }
+
+        public Dictionary<Guid, ItemByNameCounts> UserItemCounts { get; set; }
     }
 }

+ 5 - 0
MediaBrowser.Controller/Entities/Video.cs

@@ -136,6 +136,11 @@ namespace MediaBrowser.Controller.Entities
             get { return Video3DFormat.HasValue; }
         }
 
+        public bool IsHd
+        {
+            get { return MediaStreams != null && MediaStreams.Any(i => i.Type == MediaStreamType.Video && i.Width.HasValue && i.Width.Value >= 1280); }
+        }
+
         /// <summary>
         /// Gets the type of the media.
         /// </summary>

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

@@ -1,4 +1,7 @@
-
+using MediaBrowser.Model.Dto;
+using System;
+using System.Collections.Generic;
+
 namespace MediaBrowser.Controller.Entities
 {
     /// <summary>
@@ -6,6 +9,16 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public class Year : BaseItem, IItemByName
     {
+        public Year()
+        {
+            ItemCounts = new ItemByNameCounts();
+            UserItemCounts = new Dictionary<Guid, ItemByNameCounts>();
+        }
+
+        public ItemByNameCounts ItemCounts { get; set; }
+
+        public Dictionary<Guid, ItemByNameCounts> UserItemCounts { get; set; }
+
         /// <summary>
         /// Gets the user data key.
         /// </summary>

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

@@ -235,6 +235,38 @@ namespace MediaBrowser.Controller.Library
         /// <returns>Task.</returns>
         Task ValidateArtists(CancellationToken cancellationToken, IProgress<double> progress);
 
+        /// <summary>
+        /// Validates the music genres.
+        /// </summary>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="progress">The progress.</param>
+        /// <returns>Task.</returns>
+        Task ValidateMusicGenres(CancellationToken cancellationToken, IProgress<double> progress);
+
+        /// <summary>
+        /// Validates the game genres.
+        /// </summary>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="progress">The progress.</param>
+        /// <returns>Task.</returns>
+        Task ValidateGameGenres(CancellationToken cancellationToken, IProgress<double> progress);
+
+        /// <summary>
+        /// Validates the genres.
+        /// </summary>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="progress">The progress.</param>
+        /// <returns>Task.</returns>
+        Task ValidateGenres(CancellationToken cancellationToken, IProgress<double> progress);
+        
+        /// <summary>
+        /// Validates the studios.
+        /// </summary>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="progress">The progress.</param>
+        /// <returns>Task.</returns>
+        Task ValidateStudios(CancellationToken cancellationToken, IProgress<double> progress);
+        
         /// <summary>
         /// Occurs when [item added].
         /// </summary>

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

@@ -74,6 +74,7 @@
     <Compile Include="Dto\IDtoService.cs" />
     <Compile Include="Entities\AdultVideo.cs" />
     <Compile Include="Entities\Audio\IHasAlbumArtist.cs" />
+    <Compile Include="Entities\Audio\IHasMusicGenres.cs" />
     <Compile Include="Entities\Book.cs" />
     <Compile Include="Configuration\IServerConfigurationManager.cs" />
     <Compile Include="Entities\Audio\MusicGenre.cs" />
@@ -90,6 +91,7 @@
     <Compile Include="Localization\ILocalizationManager.cs" />
     <Compile Include="Notifications\INotificationsRepository.cs" />
     <Compile Include="Notifications\NotificationUpdateEventArgs.cs" />
+    <Compile Include="Providers\IDynamicInfoProvider.cs" />
     <Compile Include="Session\ISessionManager.cs" />
     <Compile Include="Drawing\ImageExtensions.cs" />
     <Compile Include="Drawing\ImageHeader.cs" />

+ 10 - 0
MediaBrowser.Controller/Providers/IDynamicInfoProvider.cs

@@ -0,0 +1,10 @@
+
+namespace MediaBrowser.Controller.Providers
+{
+    /// <summary>
+    /// Marker interface for a provider that always runs
+    /// </summary>
+    public interface IDynamicInfoProvider
+    {
+    }
+}

+ 2 - 0
MediaBrowser.Model/ApiClient/IApiClient.cs

@@ -28,6 +28,8 @@ namespace MediaBrowser.Model.ApiClient
         /// </summary>
         event EventHandler<HttpResponseEventArgs> HttpResponseReceived;
 
+        Task<T> GetAsync<T>(string url, CancellationToken cancellationToken);
+
         /// <summary>
         /// Gets the critic reviews.
         /// </summary>

+ 42 - 2
MediaBrowser.Model/Dto/BaseItemDto.cs

@@ -36,8 +36,6 @@ namespace MediaBrowser.Model.Dto
         /// <value>The name of the sort.</value>
         public string SortName { get; set; }
 
-        public string MainFeaturePlaylistName { get; set; }
-        
         /// <summary>
         /// Gets or sets the video3 D format.
         /// </summary>
@@ -521,6 +519,48 @@ namespace MediaBrowser.Model.Dto
         /// <value>The locked fields.</value>
         public List<MetadataFields> LockedFields { get; set; }
 
+        public int? AdultVideoCount { get; set; }
+        /// <summary>
+        /// Gets or sets the movie count.
+        /// </summary>
+        /// <value>The movie count.</value>
+        public int? MovieCount { get; set; }
+        /// <summary>
+        /// Gets or sets the series count.
+        /// </summary>
+        /// <value>The series count.</value>
+        public int? SeriesCount { get; set; }
+        /// <summary>
+        /// Gets or sets the episode count.
+        /// </summary>
+        /// <value>The episode count.</value>
+        public int? EpisodeCount { get; set; }
+        /// <summary>
+        /// Gets or sets the game count.
+        /// </summary>
+        /// <value>The game count.</value>
+        public int? GameCount { get; set; }
+        /// <summary>
+        /// Gets or sets the trailer count.
+        /// </summary>
+        /// <value>The trailer count.</value>
+        public int? TrailerCount { get; set; }
+        /// <summary>
+        /// Gets or sets the song count.
+        /// </summary>
+        /// <value>The song count.</value>
+        public int? SongCount { get; set; }
+        /// <summary>
+        /// Gets or sets the album count.
+        /// </summary>
+        /// <value>The album count.</value>
+        public int? AlbumCount { get; set; }
+        /// <summary>
+        /// Gets or sets the music video count.
+        /// </summary>
+        /// <value>The music video count.</value>
+        public int? MusicVideoCount { get; set; }
+        
         /// <summary>
         /// Gets or sets a value indicating whether [enable internet providers].
         /// </summary>

+ 4 - 0
MediaBrowser.Model/Dto/ItemByNameCounts.cs

@@ -11,6 +11,10 @@ namespace MediaBrowser.Model.Dto
         /// </summary>
         /// <value>The total count.</value>
         public int TotalCount { get; set; }
+        /// <summary>
+        /// Gets or sets the adult video count.
+        /// </summary>
+        /// <value>The adult video count.</value>
         public int AdultVideoCount { get; set; }
         /// <summary>
         /// Gets or sets the movie count.

+ 3 - 71
MediaBrowser.Providers/ImageFromMediaLocationProvider.cs

@@ -98,13 +98,13 @@ namespace MediaBrowser.Providers
             var args = GetResolveArgsContainingImages(item);
 
             // Make sure current image paths still exist
-            ValidateImages(item);
+            item.ValidateImages();
 
             cancellationToken.ThrowIfCancellationRequested();
 
             // Make sure current backdrop paths still exist
-            ValidateBackdrops(item);
-            ValidateScreenshots(item, args);
+            item.ValidateBackdrops();
+            item.ValidateScreenshots();
 
             cancellationToken.ThrowIfCancellationRequested();
 
@@ -128,74 +128,6 @@ namespace MediaBrowser.Providers
             return item.ResolveArgs;
         }
 
-        /// <summary>
-        /// Validates that images within the item are still on the file system
-        /// </summary>
-        /// <param name="item">The item.</param>
-        internal static void ValidateImages(BaseItem item)
-        {
-            // Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below
-            var deletedKeys = item.Images
-                .ToList()
-                .Where(image => !File.Exists(image.Value))
-                .Select(i => i.Key)
-                .ToList();
-
-            // Now remove them from the dictionary
-            foreach (var key in deletedKeys)
-            {
-                item.Images.Remove(key);
-            }
-        }
-
-        /// <summary>
-        /// Validates that backdrops within the item are still on the file system
-        /// </summary>
-        /// <param name="item">The item.</param>
-        internal static void ValidateBackdrops(BaseItem item)
-        {
-            // Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below
-            var deletedImages = item.BackdropImagePaths
-                .Where(path => !File.Exists(path))
-                .ToList();
-
-            // Now remove them from the dictionary
-            foreach (var path in deletedImages)
-            {
-                item.BackdropImagePaths.Remove(path);
-            }
-        }
-
-        /// <summary>
-        /// Validates the screenshots.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="args">The args.</param>
-        private void ValidateScreenshots(BaseItem item, ItemResolveArgs args)
-        {
-            // Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below
-            var deletedImages = item.ScreenshotImagePaths
-                .Where(path => !File.Exists(path))
-                .ToList();
-
-            // Now remove them from the dictionary
-            foreach (var path in deletedImages)
-            {
-                item.ScreenshotImagePaths.Remove(path);
-            }
-        }
-
-        /// <summary>
-        /// Determines whether [is in same directory] [the specified item].
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="path">The path.</param>
-        /// <returns><c>true</c> if [is in same directory] [the specified item]; otherwise, <c>false</c>.</returns>
-        private bool IsInMetaLocation(BaseItem item, string path)
-        {
-            return string.Equals(Path.GetDirectoryName(path), item.MetaLocation, StringComparison.OrdinalIgnoreCase);
-        }
-
         /// <summary>
         /// Gets the image.
         /// </summary>

+ 2 - 2
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -70,7 +70,6 @@
     <Compile Include="Music\AlbumInfoFromSongProvider.cs" />
     <Compile Include="Music\ArtistInfoFromSongProvider.cs" />
     <Compile Include="Music\ArtistProviderFromXml.cs" />
-    <Compile Include="Music\ArtistsPostScanTask.cs" />
     <Compile Include="Music\FanArtAlbumProvider.cs" />
     <Compile Include="Music\FanArtArtistByNameProvider.cs" />
     <Compile Include="Music\FanArtArtistProvider.cs" />
@@ -81,6 +80,7 @@
     <Compile Include="Music\LastfmArtistProvider.cs" />
     <Compile Include="Music\LastfmBaseProvider.cs" />
     <Compile Include="Music\LastfmHelper.cs" />
+    <Compile Include="Music\MusicAlbumDynamicInfoProvider.cs" />
     <Compile Include="Music\MusicVideoXmlParser.cs" />
     <Compile Include="Music\SoundtrackPostScanTask.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
@@ -106,9 +106,9 @@
     <Compile Include="TV\RemoteSeasonProvider.cs" />
     <Compile Include="TV\RemoteSeriesProvider.cs" />
     <Compile Include="TV\SeasonProviderFromXml.cs" />
+    <Compile Include="TV\SeriesPostScanTask.cs" />
     <Compile Include="TV\SeriesProviderFromXml.cs" />
     <Compile Include="TV\SeriesXmlParser.cs" />
-    <Compile Include="TV\SeriesPostScanTask.cs" />
     <Compile Include="TV\TvdbPersonImageProvider.cs" />
     <Compile Include="TV\TvdbPrescanTask.cs" />
     <Compile Include="TV\TvdbSeriesImageProvider.cs" />

+ 0 - 161
MediaBrowser.Providers/Music/ArtistsPostScanTask.cs

@@ -1,161 +0,0 @@
-using MediaBrowser.Common.Progress;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Providers.Music
-{
-    /// <summary>
-    /// Class ArtistsPostScanTask
-    /// </summary>
-    public class ArtistsPostScanTask : ILibraryPostScanTask
-    {
-        /// <summary>
-        /// The _library manager
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ArtistsPostScanTask"/> class.
-        /// </summary>
-        /// <param name="libraryManager">The library manager.</param>
-        public ArtistsPostScanTask(ILibraryManager libraryManager)
-        {
-            _libraryManager = libraryManager;
-        }
-
-        /// <summary>
-        /// Runs the specified progress.
-        /// </summary>
-        /// <param name="progress">The progress.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
-        {
-            var allItems = _libraryManager.RootFolder.RecursiveChildren.ToList();
-
-            var allArtists = await GetAllArtists(allItems).ConfigureAwait(false);
-
-            progress.Report(10);
-
-            var allMusicArtists = allItems.OfType<MusicArtist>().ToList();
-            var allSongs = allItems.OfType<Audio>().ToList();
-
-            var numComplete = 0;
-
-            foreach (var artist in allArtists)
-            {
-                var musicArtist = FindMusicArtist(artist, allMusicArtists);
-
-                if (musicArtist != null)
-                {
-                    MergeImages(musicArtist.Images, artist.Images);
-
-                    // Merge backdrops
-                    var backdrops = musicArtist.BackdropImagePaths.ToList();
-                    backdrops.InsertRange(0, artist.BackdropImagePaths);
-                    artist.BackdropImagePaths = backdrops.Distinct(StringComparer.OrdinalIgnoreCase)
-                        .ToList();
-
-                    ImageFromMediaLocationProvider.ValidateImages(artist);
-                    ImageFromMediaLocationProvider.ValidateBackdrops(artist);
-                }
-
-                if (!artist.LockedFields.Contains(MetadataFields.Genres))
-                {
-                    // Avoid implicitly captured closure
-                    var artist1 = artist;
-
-                    artist.Genres = allSongs.Where(i => i.HasArtist(artist1.Name))
-                        .SelectMany(i => i.Genres)
-                        .Distinct(StringComparer.OrdinalIgnoreCase)
-                        .ToList();
-                }
-
-                numComplete++;
-                double percent = numComplete;
-                percent /= allArtists.Length;
-                percent *= 5;
-
-                progress.Report(10 + percent);
-            }
-
-            var innerProgress = new ActionableProgress<double>();
-
-            innerProgress.RegisterAction(pct => progress.Report(15 + pct * .85));
-
-            await _libraryManager.ValidateArtists(cancellationToken, innerProgress).ConfigureAwait(false);
-        }
-
-        private void MergeImages(Dictionary<ImageType, string> source, Dictionary<ImageType, string> target)
-        {
-            foreach (var key in source.Keys
-                .ToList()
-                .Where(k => !target.ContainsKey(k)))
-            {
-                string path;
-
-                if (source.TryGetValue(key, out path))
-                {
-                    target[key] = path;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Gets all artists.
-        /// </summary>
-        /// <param name="allItems">All items.</param>
-        /// <returns>Task{Artist[]}.</returns>
-        private Task<Artist[]> GetAllArtists(IEnumerable<BaseItem> allItems)
-        {
-            var itemsList = allItems.OfType<Audio>().ToList();
-
-            var tasks = itemsList
-                .SelectMany(i =>
-                {
-                    var list = new List<string>();
-
-                    if (!string.IsNullOrEmpty(i.AlbumArtist))
-                    {
-                        list.Add(i.AlbumArtist);
-                    }
-                    list.AddRange(i.Artists);
-
-                    return list;
-                })
-                .Distinct(StringComparer.OrdinalIgnoreCase)
-                .Select(i => _libraryManager.GetArtist(i));
-
-            return Task.WhenAll(tasks);
-        }
-
-        /// <summary>
-        /// Finds the music artist.
-        /// </summary>
-        /// <param name="artist">The artist.</param>
-        /// <param name="allMusicArtists">All music artists.</param>
-        /// <returns>MusicArtist.</returns>
-        private static MusicArtist FindMusicArtist(Artist artist, IEnumerable<MusicArtist> allMusicArtists)
-        {
-            var musicBrainzId = artist.GetProviderId(MetadataProviders.Musicbrainz);
-
-            return allMusicArtists.FirstOrDefault(i =>
-            {
-                if (!string.IsNullOrWhiteSpace(musicBrainzId) && string.Equals(musicBrainzId, i.GetProviderId(MetadataProviders.Musicbrainz), StringComparison.OrdinalIgnoreCase))
-                {
-                    return true;
-                }
-
-                return string.Compare(i.Name, artist.Name, CultureInfo.CurrentCulture, CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols) == 0;
-            });
-        }
-    }
-}

+ 85 - 0
MediaBrowser.Providers/Music/MusicAlbumDynamicInfoProvider.cs

@@ -0,0 +1,85 @@
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.Music
+{
+    /// <summary>
+    /// Class MusicAlbumDynamicInfoProvider
+    /// </summary>
+    public class MusicAlbumDynamicInfoProvider : BaseMetadataProvider, IDynamicInfoProvider
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="BaseMetadataProvider" /> class.
+        /// </summary>
+        /// <param name="logManager">The log manager.</param>
+        /// <param name="configurationManager">The configuration manager.</param>
+        public MusicAlbumDynamicInfoProvider(ILogManager logManager, IServerConfigurationManager configurationManager)
+            : base(logManager, configurationManager)
+        {
+        }
+
+        /// <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 MusicAlbum;
+        }
+
+        /// <summary>
+        /// Needses the refresh internal.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="providerInfo">The provider info.</param>
+        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+        protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
+        {
+            return true;
+        }
+
+        /// <summary>
+        /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="force">if set to <c>true</c> [force].</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{System.Boolean}.</returns>
+        public override Task<bool> FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken)
+        {
+            var album = (MusicAlbum)item;
+
+            var songs = album.RecursiveChildren
+                             .OfType<Audio>()
+                             .ToList();
+
+            album.AlbumArtist = songs
+                .Select(i => i.AlbumArtist)
+                .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+
+            album.Artists = songs.SelectMany(i => i.Artists)
+                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .ToArray();
+
+            // Don't save to the db
+            return FalseTaskResult;
+        }
+
+        /// <summary>
+        /// Gets the priority.
+        /// </summary>
+        /// <value>The priority.</value>
+        public override MetadataProviderPriority Priority
+        {
+            get { return MetadataProviderPriority.Last; }
+        }
+    }
+}

+ 48 - 8
MediaBrowser.Server.Implementations/Dto/DtoService.cs

@@ -107,6 +107,15 @@ namespace MediaBrowser.Server.Implementations.Dto
                     .ToArray();
             }
 
+            if (fields.Contains(ItemFields.ItemCounts))
+            {
+                var itemByName = item as IItemByName;
+                if (itemByName != null)
+                {
+                    AttachItemByNameCounts(dto, itemByName, user);
+                }
+            }
+
             // Make sure all the tasks we kicked off have completed.
             if (tasks.Count > 0)
             {
@@ -116,6 +125,41 @@ namespace MediaBrowser.Server.Implementations.Dto
             return dto;
         }
 
+        /// <summary>
+        /// Attaches the item by name counts.
+        /// </summary>
+        /// <param name="dto">The dto.</param>
+        /// <param name="item">The item.</param>
+        /// <param name="user">The user.</param>
+        private void AttachItemByNameCounts(BaseItemDto dto, IItemByName item, User user)
+        {
+            ItemByNameCounts counts;
+
+            if (user == null)
+            {
+                counts = item.ItemCounts;
+            }
+            else
+            {
+                if (!item.UserItemCounts.TryGetValue(user.Id, out counts))
+                {
+                    counts = new ItemByNameCounts();
+                }
+            }
+
+            dto.ChildCount = counts.TotalCount;
+
+            dto.AdultVideoCount = counts.AdultVideoCount;
+            dto.AlbumCount = counts.AlbumCount;
+            dto.EpisodeCount = counts.EpisodeCount;
+            dto.GameCount = counts.GameCount;
+            dto.MovieCount = counts.MovieCount;
+            dto.MusicVideoCount = counts.MusicVideoCount;
+            dto.SeriesCount = counts.SeriesCount;
+            dto.SongCount = counts.SongCount;
+            dto.TrailerCount = counts.TrailerCount;
+        }
+
         /// <summary>
         /// Attaches the user specific info.
         /// </summary>
@@ -380,7 +424,9 @@ namespace MediaBrowser.Server.Implementations.Dto
                 _logger.ErrorException("Error getting {0} image info for {1}", ex, type, path);
                 return null;
             }
-        }        /// <summary>
+        }        
+        
+        /// <summary>
         /// Attaches People DTO's to a DTOBaseItem
         /// </summary>
         /// <param name="dto">The dto.</param>
@@ -913,12 +959,7 @@ namespace MediaBrowser.Server.Implementations.Dto
 
             if (album != null)
             {
-                var songs = album.RecursiveChildren.OfType<Audio>().ToList();
-
-                dto.Artists =
-                    songs.SelectMany(i => i.Artists)
-                         .Distinct(StringComparer.OrdinalIgnoreCase)
-                         .ToArray();
+                dto.Artists = album.Artists;
             }
 
             var hasAlbumArtist = item as IHasAlbumArtist;
@@ -935,7 +976,6 @@ namespace MediaBrowser.Server.Implementations.Dto
                 dto.VideoType = video.VideoType;
                 dto.Video3DFormat = video.Video3DFormat;
                 dto.IsoType = video.IsoType;
-                dto.MainFeaturePlaylistName = video.MainFeaturePlaylistName;
 
                 dto.PartCount = video.AdditionalPartIds.Count + 1;
 

+ 4 - 0
MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs

@@ -81,6 +81,10 @@ namespace MediaBrowser.Server.Implementations.HttpServer
                 {
                     bytes = await ReceiveBytesAsync(CancellationToken.None).ConfigureAwait(false);
                 }
+                catch (OperationCanceledException)
+                {
+                    break;
+                }
                 catch (WebSocketException ex)
                 {
                     _logger.ErrorException("Error receiving web socket message", ex);

+ 129 - 90
MediaBrowser.Server.Implementations/Library/LibraryManager.cs

@@ -13,6 +13,7 @@ using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
+using MediaBrowser.Server.Implementations.Library.Validators;
 using MediaBrowser.Server.Implementations.ScheduledTasks;
 using MoreLinq;
 using System;
@@ -597,11 +598,11 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <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>
+        /// <param name="refreshMetadata">if set to <c>true</c> [force creation].</param>
         /// <returns>Task{Person}.</returns>
-        private Task<Person> GetPerson(string name, CancellationToken cancellationToken, bool allowSlowProviders = false, bool forceCreation = false)
+        private Task<Person> GetPerson(string name, CancellationToken cancellationToken, bool allowSlowProviders = false, bool refreshMetadata = false)
         {
-            return GetItemByName<Person>(ConfigurationManager.ApplicationPaths.PeoplePath, name, cancellationToken, allowSlowProviders, forceCreation);
+            return GetItemByName<Person>(ConfigurationManager.ApplicationPaths.PeoplePath, name, cancellationToken, allowSlowProviders, refreshMetadata);
         }
 
         /// <summary>
@@ -612,7 +613,20 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <returns>Task{Studio}.</returns>
         public Task<Studio> GetStudio(string name, bool allowSlowProviders = false)
         {
-            return GetItemByName<Studio>(ConfigurationManager.ApplicationPaths.StudioPath, name, CancellationToken.None, allowSlowProviders);
+            return GetStudio(name, CancellationToken.None, allowSlowProviders);
+        }
+
+        /// <summary>
+        /// Gets the studio.
+        /// </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="refreshMetadata">if set to <c>true</c> [refresh metadata].</param>
+        /// <returns>Task{Studio}.</returns>
+        internal Task<Studio> GetStudio(string name, CancellationToken cancellationToken, bool allowSlowProviders = false, bool refreshMetadata = false)
+        {
+            return GetItemByName<Studio>(ConfigurationManager.ApplicationPaths.StudioPath, name, cancellationToken, allowSlowProviders, refreshMetadata);
         }
 
         /// <summary>
@@ -623,7 +637,20 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <returns>Task{Genre}.</returns>
         public Task<Genre> GetGenre(string name, bool allowSlowProviders = false)
         {
-            return GetItemByName<Genre>(ConfigurationManager.ApplicationPaths.GenrePath, name, CancellationToken.None, allowSlowProviders);
+            return GetGenre(name, CancellationToken.None, allowSlowProviders);
+        }
+
+        /// <summary>
+        /// Gets the genre.
+        /// </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="refreshMetadata">if set to <c>true</c> [refresh metadata].</param>
+        /// <returns>Task{Genre}.</returns>
+        internal Task<Genre> GetGenre(string name, CancellationToken cancellationToken, bool allowSlowProviders = false, bool refreshMetadata = false)
+        {
+            return GetItemByName<Genre>(ConfigurationManager.ApplicationPaths.GenrePath, name, cancellationToken, allowSlowProviders, refreshMetadata);
         }
 
         /// <summary>
@@ -634,7 +661,20 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <returns>Task{MusicGenre}.</returns>
         public Task<MusicGenre> GetMusicGenre(string name, bool allowSlowProviders = false)
         {
-            return GetItemByName<MusicGenre>(ConfigurationManager.ApplicationPaths.MusicGenrePath, name, CancellationToken.None, allowSlowProviders);
+            return GetMusicGenre(name, CancellationToken.None, allowSlowProviders);
+        }
+
+        /// <summary>
+        /// Gets the music genre.
+        /// </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="refreshMetadata">if set to <c>true</c> [refresh metadata].</param>
+        /// <returns>Task{MusicGenre}.</returns>
+        internal Task<MusicGenre> GetMusicGenre(string name, CancellationToken cancellationToken, bool allowSlowProviders = false, bool refreshMetadata = false)
+        {
+            return GetItemByName<MusicGenre>(ConfigurationManager.ApplicationPaths.MusicGenrePath, name, cancellationToken, allowSlowProviders, refreshMetadata);
         }
 
         /// <summary>
@@ -645,7 +685,20 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <returns>Task{GameGenre}.</returns>
         public Task<GameGenre> GetGameGenre(string name, bool allowSlowProviders = false)
         {
-            return GetItemByName<GameGenre>(ConfigurationManager.ApplicationPaths.GameGenrePath, name, CancellationToken.None, allowSlowProviders);
+            return GetGameGenre(name, CancellationToken.None, allowSlowProviders);
+        }
+
+        /// <summary>
+        /// Gets the game genre.
+        /// </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="refreshMetadata">if set to <c>true</c> [refresh metadata].</param>
+        /// <returns>Task{GameGenre}.</returns>
+        internal Task<GameGenre> GetGameGenre(string name, CancellationToken cancellationToken, bool allowSlowProviders = false, bool refreshMetadata = false)
+        {
+            return GetItemByName<GameGenre>(ConfigurationManager.ApplicationPaths.GameGenrePath, name, cancellationToken, allowSlowProviders, refreshMetadata);
         }
 
         /// <summary>
@@ -665,11 +718,11 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <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>
+        /// <param name="refreshMetadata">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)
+        internal Task<Artist> GetArtist(string name, CancellationToken cancellationToken, bool allowSlowProviders = false, bool refreshMetadata = false)
         {
-            return GetItemByName<Artist>(ConfigurationManager.ApplicationPaths.ArtistsPath, name, cancellationToken, allowSlowProviders, forceCreation);
+            return GetItemByName<Artist>(ConfigurationManager.ApplicationPaths.ArtistsPath, name, cancellationToken, allowSlowProviders, refreshMetadata);
         }
 
         /// <summary>
@@ -707,11 +760,11 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <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>
+        /// <param name="refreshMetadata">if set to <c>true</c> [force creation].</param>
         /// <returns>Task{``0}.</returns>
         /// <exception cref="System.ArgumentNullException">
         /// </exception>
-        private async Task<T> GetItemByName<T>(string path, string name, CancellationToken cancellationToken, bool allowSlowProviders = true, bool forceCreation = false)
+        private async Task<T> GetItemByName<T>(string path, string name, CancellationToken cancellationToken, bool allowSlowProviders = true, bool refreshMetadata = false)
             where T : BaseItem, new()
         {
             if (string.IsNullOrEmpty(path))
@@ -730,11 +783,25 @@ namespace MediaBrowser.Server.Implementations.Library
 
             if (!_itemsByName.TryGetValue(key, out obj))
             {
-                obj = await CreateItemByName<T>(path, name, cancellationToken, allowSlowProviders).ConfigureAwait(false);
+                var tuple = CreateItemByName<T>(path, name, cancellationToken);
+
+                obj = tuple.Item2;
 
                 _itemsByName.AddOrUpdate(key, obj, (keyName, oldValue) => obj);
+
+                try
+                {
+                    await obj.RefreshMetadata(cancellationToken, tuple.Item1, allowSlowProviders: allowSlowProviders).ConfigureAwait(false);
+                }
+                catch (OperationCanceledException)
+                {
+                    BaseItem removed;
+                    _itemsByName.TryRemove(key, out removed);
+
+                    throw;
+                }
             }
-            else if (forceCreation)
+            else if (refreshMetadata)
             {
                 await obj.RefreshMetadata(cancellationToken, false, allowSlowProviders: allowSlowProviders).ConfigureAwait(false);
             }
@@ -749,10 +816,9 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <param name="path">The path.</param>
         /// <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>
         /// <returns>Task{``0}.</returns>
         /// <exception cref="System.IO.IOException">Path not created:  + path</exception>
-        private async Task<T> CreateItemByName<T>(string path, string name, CancellationToken cancellationToken, bool allowSlowProviders = true)
+        private Tuple<bool, T> CreateItemByName<T>(string path, string name, CancellationToken cancellationToken)
             where T : BaseItem, new()
         {
             cancellationToken.ThrowIfCancellationRequested();
@@ -783,6 +849,7 @@ namespace MediaBrowser.Server.Implementations.Library
             var id = path.GetMBId(type);
 
             var item = RetrieveItem(id) as T;
+
             if (item == null)
             {
                 item = new T
@@ -796,16 +863,10 @@ namespace MediaBrowser.Server.Implementations.Library
                 isNew = true;
             }
 
-            cancellationToken.ThrowIfCancellationRequested();
-
             // Set this now so we don't cause additional file system access during provider executions
             item.ResetResolveArgs(fileInfo);
 
-            await item.RefreshMetadata(cancellationToken, isNew, allowSlowProviders: allowSlowProviders).ConfigureAwait(false);
-
-            cancellationToken.ThrowIfCancellationRequested();
-
-            return item;
+            return new Tuple<bool,T>(isNew, item);
         }
 
         /// <summary>
@@ -884,75 +945,53 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="progress">The progress.</param>
         /// <returns>Task.</returns>
-        public async Task ValidateArtists(CancellationToken cancellationToken, IProgress<double> progress)
+        public 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 = new List<string>();
-
-                    if (!string.IsNullOrEmpty(c.AlbumArtist))
-                    {
-                        list.Add(c.AlbumArtist);
-                    }
-                    list.AddRange(c.Artists);
-
-                    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;
+            return new ArtistsValidator(this, _userManager, _logger).Run(progress, cancellationToken);
+        }
 
-                        progress.Report(100 * percent);
-                    }
-                }));
-            }
+        /// <summary>
+        /// Validates the music genres.
+        /// </summary>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="progress">The progress.</param>
+        /// <returns>Task.</returns>
+        public Task ValidateMusicGenres(CancellationToken cancellationToken, IProgress<double> progress)
+        {
+            return new MusicGenresValidator(this, _userManager, _logger).Run(progress, cancellationToken);
+        }
 
-            await Task.WhenAll(tasks).ConfigureAwait(false);
+        /// <summary>
+        /// Validates the game genres.
+        /// </summary>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="progress">The progress.</param>
+        /// <returns>Task.</returns>
+        public Task ValidateGameGenres(CancellationToken cancellationToken, IProgress<double> progress)
+        {
+            return new GameGenresValidator(this, _userManager, _logger).Run(progress, cancellationToken);
+        }
 
-            progress.Report(100);
+        /// <summary>
+        /// Validates the studios.
+        /// </summary>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="progress">The progress.</param>
+        /// <returns>Task.</returns>
+        public Task ValidateStudios(CancellationToken cancellationToken, IProgress<double> progress)
+        {
+            return new StudiosValidator(this, _userManager, _logger).Run(progress, cancellationToken);
+        }
 
-            _logger.Info("Artist validation complete");
+        /// <summary>
+        /// Validates the genres.
+        /// </summary>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="progress">The progress.</param>
+        /// <returns>Task.</returns>
+        public Task ValidateGenres(CancellationToken cancellationToken, IProgress<double> progress)
+        {
+            return new GenresValidator(this, _userManager, _logger).Run(progress, cancellationToken);
         }
 
         /// <summary>
@@ -1000,12 +1039,12 @@ namespace MediaBrowser.Server.Implementations.Library
 
             var innerProgress = new ActionableProgress<double>();
 
-            innerProgress.RegisterAction(pct => progress.Report(15 + pct * .65));
+            innerProgress.RegisterAction(pct => progress.Report(15 + pct * .6));
 
             // Now validate the entire media library
             await RootFolder.ValidateChildren(innerProgress, cancellationToken, recursive: true).ConfigureAwait(false);
 
-            progress.Report(80);
+            progress.Report(75);
 
             // Run post-scan tasks
             await RunPostScanTasks(progress, cancellationToken).ConfigureAwait(false);
@@ -1078,7 +1117,7 @@ namespace MediaBrowser.Server.Implementations.Library
                         double percent = progressDictionary.Values.Sum();
                         percent /= postscanTasks.Count;
 
-                        progress.Report(80 + percent * .2);
+                        progress.Report(75 + percent * .25);
                     }
                 });
 

+ 2 - 2
MediaBrowser.Server.Implementations/Library/LuceneSearchEngine.cs

@@ -155,7 +155,7 @@ namespace MediaBrowser.Server.Implementations.Library
             }
 
             // Find genres, from non-audio items
-            var genres = items.Where(i => !(i is Audio) && !(i is MusicAlbum) && !(i is MusicArtist) && !(i is MusicVideo) && !(i is Game))
+            var genres = items.Where(i => !(i is IHasMusicGenres) && !(i is Game))
                 .SelectMany(i => i.Genres)
                 .Where(i => !string.IsNullOrEmpty(i))
                 .Distinct(StringComparer.OrdinalIgnoreCase)
@@ -181,7 +181,7 @@ namespace MediaBrowser.Server.Implementations.Library
             }
 
             // Find music genres
-            var musicGenres = items.Where(i => (i is Audio) || (i is MusicAlbum) || (i is MusicArtist) || (i is MusicVideo))
+            var musicGenres = items.Where(i => i is IHasMusicGenres)
                 .SelectMany(i => i.Genres)
                 .Where(i => !string.IsNullOrEmpty(i))
                 .Distinct(StringComparer.OrdinalIgnoreCase)

+ 38 - 0
MediaBrowser.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs

@@ -0,0 +1,38 @@
+using MediaBrowser.Controller.Library;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Library.Validators
+{
+    /// <summary>
+    /// Class ArtistsPostScanTask
+    /// </summary>
+    public class ArtistsPostScanTask : ILibraryPostScanTask
+    {
+        /// <summary>
+        /// The _library manager
+        /// </summary>
+        private readonly ILibraryManager _libraryManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
+        /// </summary>
+        /// <param name="libraryManager">The library manager.</param>
+        public ArtistsPostScanTask(ILibraryManager libraryManager)
+        {
+            _libraryManager = libraryManager;
+        }
+
+        /// <summary>
+        /// Runs the specified progress.
+        /// </summary>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            return _libraryManager.ValidateArtists(cancellationToken, progress);
+        }
+    }
+}

+ 285 - 0
MediaBrowser.Server.Implementations/Library/Validators/ArtistsValidator.cs

@@ -0,0 +1,285 @@
+using MediaBrowser.Common.Progress;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Library.Validators
+{
+    /// <summary>
+    /// Class ArtistsValidator
+    /// </summary>
+    public class ArtistsValidator
+    {
+        /// <summary>
+        /// The _library manager
+        /// </summary>
+        private readonly LibraryManager _libraryManager;
+
+        /// <summary>
+        /// The _user manager
+        /// </summary>
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// The _logger
+        /// </summary>
+        private readonly ILogger _logger;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
+        /// </summary>
+        /// <param name="libraryManager">The library manager.</param>
+        /// <param name="userManager">The user manager.</param>
+        /// <param name="logger">The logger.</param>
+        public ArtistsValidator(LibraryManager libraryManager, IUserManager userManager, ILogger logger)
+        {
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+            _logger = logger;
+        }
+
+        /// <summary>
+        /// Runs the specified progress.
+        /// </summary>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            var allItems = _libraryManager.RootFolder.RecursiveChildren.ToList();
+
+            var allMusicArtists = allItems.OfType<MusicArtist>().ToList();
+            var allSongs = allItems.OfType<Audio>().ToList();
+
+            var innerProgress = new ActionableProgress<double>();
+
+            innerProgress.RegisterAction(pct => progress.Report(pct * .8));
+            
+            var allArtists = await GetAllArtists(allSongs, cancellationToken, innerProgress).ConfigureAwait(false);
+
+            progress.Report(80);
+
+            var numComplete = 0;
+
+            var userLibraries = _userManager.Users
+                .Select(i => new Tuple<Guid, List<IHasArtist>>(i.Id, i.RootFolder.GetRecursiveChildren(i).OfType<IHasArtist>().ToList()))
+                .ToList();
+
+            foreach (var artist in allArtists)
+            {
+                cancellationToken.ThrowIfCancellationRequested();
+                
+                artist.ValidateImages();
+                artist.ValidateBackdrops();
+
+                var musicArtist = FindMusicArtist(artist, allMusicArtists);
+
+                if (musicArtist != null)
+                {
+                    MergeImages(musicArtist.Images, artist.Images);
+
+                    // Merge backdrops
+                    var backdrops = musicArtist.BackdropImagePaths.ToList();
+                    backdrops.InsertRange(0, artist.BackdropImagePaths);
+                    artist.BackdropImagePaths = backdrops.Distinct(StringComparer.OrdinalIgnoreCase)
+                        .ToList();
+                }
+
+                if (!artist.LockedFields.Contains(MetadataFields.Genres))
+                {
+                    // Avoid implicitly captured closure
+                    var artist1 = artist;
+
+                    artist.Genres = allSongs.Where(i => i.HasArtist(artist1.Name))
+                        .SelectMany(i => i.Genres)
+                        .Distinct(StringComparer.OrdinalIgnoreCase)
+                        .ToList();
+                }
+
+                // Populate counts of items
+                SetItemCounts(artist, null, allItems.OfType<IHasArtist>());
+
+                foreach (var lib in userLibraries)
+                {
+                    SetItemCounts(artist, lib.Item1, lib.Item2);
+                }
+
+                numComplete++;
+                double percent = numComplete;
+                percent /= allArtists.Count;
+                percent *= 20;
+
+                progress.Report(80 + percent);
+            }
+
+            progress.Report(100);
+        }
+
+        /// <summary>
+        /// Sets the item counts.
+        /// </summary>
+        /// <param name="artist">The artist.</param>
+        /// <param name="userId">The user id.</param>
+        /// <param name="allItems">All items.</param>
+        private void SetItemCounts(Artist artist, Guid? userId, IEnumerable<IHasArtist> allItems)
+        {
+            var name = artist.Name;
+
+            var items = allItems
+                .Where(i => i.HasArtist(name))
+                .ToList();
+
+            var counts = new ItemByNameCounts
+            {
+                TotalCount = items.Count,
+
+                SongCount = items.OfType<Audio>().Count(),
+
+                AlbumCount = items.OfType<MusicAlbum>().Count(),
+
+                MusicVideoCount = items.OfType<MusicVideo>().Count()
+            };
+
+            if (userId.HasValue)
+            {
+                artist.UserItemCounts[userId.Value] = counts;
+            }
+            else
+            {
+                artist.ItemCounts = counts;
+            }
+        }
+
+        /// <summary>
+        /// Merges the images.
+        /// </summary>
+        /// <param name="source">The source.</param>
+        /// <param name="target">The target.</param>
+        private void MergeImages(Dictionary<ImageType, string> source, Dictionary<ImageType, string> target)
+        {
+            foreach (var key in source.Keys
+                .ToList()
+                .Where(k => !target.ContainsKey(k)))
+            {
+                string path;
+
+                if (source.TryGetValue(key, out path))
+                {
+                    target[key] = path;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets all artists.
+        /// </summary>
+        /// <param name="allSongs">All songs.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="progress">The progress.</param>
+        /// <returns>Task{Artist[]}.</returns>
+        private async Task<List<Artist>> GetAllArtists(IEnumerable<Audio> allSongs, CancellationToken cancellationToken, IProgress<double> progress)
+        {
+            var allArtists = allSongs
+                .SelectMany(i =>
+                {
+                    var list = new List<string>();
+
+                    if (!string.IsNullOrEmpty(i.AlbumArtist))
+                    {
+                        list.Add(i.AlbumArtist);
+                    }
+                    list.AddRange(i.Artists);
+
+                    return list;
+                })
+                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .ToList();
+
+            const int maxTasks = 5;
+
+            var tasks = new List<Task>();
+
+            var returnArtists = new ConcurrentBag<Artist>();
+
+            var numComplete = 0;
+
+            foreach (var artist in allArtists)
+            {
+                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
+                    {
+                        var artistItem = await _libraryManager.GetArtist(currentArtist, cancellationToken, true, true)
+                            .ConfigureAwait(false);
+
+                        returnArtists.Add(artistItem);
+                    }
+                    catch (IOException ex)
+                    {
+                        _logger.ErrorException("Error validating Artist {0}", ex, currentArtist);
+                    }
+
+                    // Update progress
+                    lock (progress)
+                    {
+                        numComplete++;
+                        double percent = numComplete;
+                        percent /= allArtists.Count;
+
+                        progress.Report(100 * percent);
+                    }
+                }));
+            }
+
+            await Task.WhenAll(tasks).ConfigureAwait(false);
+
+            return returnArtists.ToList();
+        }
+
+        /// <summary>
+        /// Finds the music artist.
+        /// </summary>
+        /// <param name="artist">The artist.</param>
+        /// <param name="allMusicArtists">All music artists.</param>
+        /// <returns>MusicArtist.</returns>
+        private static MusicArtist FindMusicArtist(Artist artist, IEnumerable<MusicArtist> allMusicArtists)
+        {
+            var musicBrainzId = artist.GetProviderId(MetadataProviders.Musicbrainz);
+
+            return allMusicArtists.FirstOrDefault(i =>
+            {
+                if (!string.IsNullOrWhiteSpace(musicBrainzId) && string.Equals(musicBrainzId, i.GetProviderId(MetadataProviders.Musicbrainz), StringComparison.OrdinalIgnoreCase))
+                {
+                    return true;
+                }
+
+                return string.Compare(i.Name, artist.Name, CultureInfo.CurrentCulture, CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols) == 0;
+            });
+        }
+    }
+}

+ 155 - 0
MediaBrowser.Server.Implementations/Library/Validators/CountHelpers.cs

@@ -0,0 +1,155 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Model.Dto;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Server.Implementations.Library.Validators
+{
+    /// <summary>
+    /// Class CountHelpers
+    /// </summary>
+    internal static class CountHelpers
+    {
+        /// <summary>
+        /// Adds to dictionary.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="counts">The counts.</param>
+        internal static void AddToDictionary(BaseItem item, Dictionary<string, int> counts)
+        {
+            if (item is Movie)
+            {
+                IncrementCount(counts, "Movie");
+            }
+            else if (item is Trailer)
+            {
+                IncrementCount(counts, "Trailer");
+            }
+            else if (item is Series)
+            {
+                IncrementCount(counts, "Series");
+            }
+            else if (item is Game)
+            {
+                IncrementCount(counts, "Game");
+            }
+            else if (item is Audio)
+            {
+                IncrementCount(counts, "Audio");
+            }
+            else if (item is MusicAlbum)
+            {
+                IncrementCount(counts, "MusicAlbum");
+            }
+            else if (item is Episode)
+            {
+                IncrementCount(counts, "Episode");
+            }
+            else if (item is MusicVideo)
+            {
+                IncrementCount(counts, "MusicVideo");
+            }
+            else if (item is AdultVideo)
+            {
+                IncrementCount(counts, "AdultVideo");
+            }
+
+            IncrementCount(counts, "Total");
+        }
+
+        /// <summary>
+        /// Increments the count.
+        /// </summary>
+        /// <param name="counts">The counts.</param>
+        /// <param name="key">The key.</param>
+        internal static void IncrementCount(Dictionary<string, int> counts, string key)
+        {
+            int count;
+
+            if (counts.TryGetValue(key, out count))
+            {
+                count++;
+                counts[key] = count;
+            }
+            else
+            {
+                counts.Add(key, 1);
+            }
+        }
+
+        /// <summary>
+        /// Gets the counts.
+        /// </summary>
+        /// <param name="counts">The counts.</param>
+        /// <returns>ItemByNameCounts.</returns>
+        internal static ItemByNameCounts GetCounts(Dictionary<string, int> counts)
+        {
+            return new ItemByNameCounts
+            {
+                AdultVideoCount = GetCount(counts, "AdultVideo"),
+                AlbumCount = GetCount(counts, "MusicAlbum"),
+                EpisodeCount = GetCount(counts, "Episode"),
+                GameCount = GetCount(counts, "Game"),
+                MovieCount = GetCount(counts, "Movie"),
+                MusicVideoCount = GetCount(counts, "MusicVideo"),
+                SeriesCount = GetCount(counts, "Series"),
+                SongCount = GetCount(counts, "Audio"),
+                TrailerCount = GetCount(counts, "Trailer"),
+                TotalCount = GetCount(counts, "Total")
+            };
+        }
+
+        /// <summary>
+        /// Gets the count.
+        /// </summary>
+        /// <param name="counts">The counts.</param>
+        /// <param name="key">The key.</param>
+        /// <returns>System.Int32.</returns>
+        internal static int GetCount(Dictionary<string, int> counts, string key)
+        {
+            int count;
+
+            if (counts.TryGetValue(key, out count))
+            {
+                return count;
+            }
+
+            return 0;
+        }
+
+        /// <summary>
+        /// Sets the item counts.
+        /// </summary>
+        /// <param name="userId">The user id.</param>
+        /// <param name="media">The media.</param>
+        /// <param name="names">The names.</param>
+        /// <param name="masterDictionary">The master dictionary.</param>
+        internal static void SetItemCounts(Guid? userId, BaseItem media, List<string> names, Dictionary<string, Dictionary<Guid, Dictionary<string, int>>> masterDictionary)
+        {
+            foreach (var name in names)
+            {
+                Dictionary<Guid, Dictionary<string, int>> libraryCounts;
+
+                if (!masterDictionary.TryGetValue(name, out libraryCounts))
+                {
+                    libraryCounts = new Dictionary<Guid, Dictionary<string, int>>();
+                    masterDictionary.Add(name, libraryCounts);
+                }
+
+                var userLibId = userId ?? Guid.Empty;
+                Dictionary<string, int> userDictionary;
+
+                if (!libraryCounts.TryGetValue(userLibId, out userDictionary))
+                {
+                    userDictionary = new Dictionary<string, int>();
+                    libraryCounts.Add(userLibId, userDictionary);
+                }
+
+                AddToDictionary(media, userDictionary);
+            }
+        }
+    }
+}

+ 45 - 0
MediaBrowser.Server.Implementations/Library/Validators/GameGenresPostScanTask.cs

@@ -0,0 +1,45 @@
+using MediaBrowser.Controller.Library;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Library.Validators
+{
+    /// <summary>
+    /// Class GameGenresPostScanTask
+    /// </summary>
+    public class GameGenresPostScanTask : ILibraryPostScanTask
+    {
+        /// <summary>
+        /// The _library manager
+        /// </summary>
+        private readonly ILibraryManager _libraryManager;
+
+        /// <summary>
+        /// The _user manager
+        /// </summary>
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="GameGenresPostScanTask"/> class.
+        /// </summary>
+        /// <param name="libraryManager">The library manager.</param>
+        /// <param name="userManager">The user manager.</param>
+        public GameGenresPostScanTask(ILibraryManager libraryManager, IUserManager userManager)
+        {
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+        }
+
+        /// <summary>
+        /// Runs the specified progress.
+        /// </summary>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            return _libraryManager.ValidateGameGenres(cancellationToken, progress);
+        }
+    }
+}

+ 132 - 0
MediaBrowser.Server.Implementations/Library/Validators/GameGenresValidator.cs

@@ -0,0 +1,132 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Library.Validators
+{
+    class GameGenresValidator
+    {
+        /// <summary>
+        /// The _library manager
+        /// </summary>
+        private readonly LibraryManager _libraryManager;
+
+        /// <summary>
+        /// The _user manager
+        /// </summary>
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// The _logger
+        /// </summary>
+        private readonly ILogger _logger;
+
+        public GameGenresValidator(LibraryManager libraryManager, IUserManager userManager, ILogger logger)
+        {
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+            _logger = logger;
+        }
+
+        /// <summary>
+        /// Runs the specified progress.
+        /// </summary>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            var allItems = _libraryManager.RootFolder.RecursiveChildren.OfType<Game>().ToList();
+
+            var userLibraries = _userManager.Users
+                .Select(i => new Tuple<Guid, List<Game>>(i.Id, i.RootFolder.GetRecursiveChildren(i).OfType<Game>().ToList()))
+                .ToList();
+
+            var allLibraryItems = allItems;
+
+            var masterDictionary = new Dictionary<string, Dictionary<Guid, Dictionary<string, int>>>(StringComparer.OrdinalIgnoreCase);
+
+            // Populate counts of items
+            SetItemCounts(null, allLibraryItems, masterDictionary);
+
+            progress.Report(2);
+
+            var numComplete = 0;
+
+            foreach (var lib in userLibraries)
+            {
+                SetItemCounts(lib.Item1, lib.Item2, masterDictionary);
+
+                numComplete++;
+                double percent = numComplete;
+                percent /= userLibraries.Count;
+                percent *= 8;
+
+                progress.Report(percent);
+            }
+
+            progress.Report(10);
+
+            var names = masterDictionary.Keys.ToList();
+            numComplete = 0;
+
+            foreach (var name in names)
+            {
+                try
+                {
+                    await UpdateItemByNameCounts(name, cancellationToken, masterDictionary[name]).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error updating counts for {0}", ex, name);
+                }
+
+                numComplete++;
+                double percent = numComplete;
+                percent /= names.Count;
+                percent *= 90;
+
+                progress.Report(percent + 10);
+            }
+
+            progress.Report(100);
+        }
+
+        private async Task UpdateItemByNameCounts(string name, CancellationToken cancellationToken, Dictionary<Guid, Dictionary<string, int>> counts)
+        {
+            var itemByName = await _libraryManager.GetGameGenre(name, cancellationToken, true, true).ConfigureAwait(false);
+
+            foreach (var libraryId in counts.Keys.ToList())
+            {
+                var itemCounts = CountHelpers.GetCounts(counts[libraryId]);
+
+                if (libraryId == Guid.Empty)
+                {
+                    itemByName.ItemCounts = itemCounts;
+                }
+                else
+                {
+                    itemByName.UserItemCounts[libraryId] = itemCounts;
+                }
+            }
+        }
+
+        private void SetItemCounts(Guid? userId, IEnumerable<BaseItem> allItems, Dictionary<string, Dictionary<Guid, Dictionary<string, int>>> masterDictionary)
+        {
+            foreach (var media in allItems)
+            {
+                var names = media
+                    .Genres
+                    .Distinct(StringComparer.OrdinalIgnoreCase)
+                    .ToList();
+
+                CountHelpers.SetItemCounts(userId, media, names, masterDictionary);
+            }
+        }
+    }
+}

+ 42 - 0
MediaBrowser.Server.Implementations/Library/Validators/GenresPostScanTask.cs

@@ -0,0 +1,42 @@
+using MediaBrowser.Controller.Library;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Library.Validators
+{
+    public class GenresPostScanTask : ILibraryPostScanTask
+    {
+        /// <summary>
+        /// The _library manager
+        /// </summary>
+        private readonly ILibraryManager _libraryManager;
+
+        /// <summary>
+        /// The _user manager
+        /// </summary>
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
+        /// </summary>
+        /// <param name="libraryManager">The library manager.</param>
+        /// <param name="userManager">The user manager.</param>
+        public GenresPostScanTask(ILibraryManager libraryManager, IUserManager userManager)
+        {
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+        }
+
+        /// <summary>
+        /// Runs the specified progress.
+        /// </summary>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            return _libraryManager.ValidateGenres(cancellationToken, progress);
+        }
+    }
+}

+ 135 - 0
MediaBrowser.Server.Implementations/Library/Validators/GenresValidator.cs

@@ -0,0 +1,135 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Library.Validators
+{
+    class GenresValidator
+    {
+        /// <summary>
+        /// The _library manager
+        /// </summary>
+        private readonly LibraryManager _libraryManager;
+
+        /// <summary>
+        /// The _user manager
+        /// </summary>
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// The _logger
+        /// </summary>
+        private readonly ILogger _logger;
+
+        public GenresValidator(LibraryManager libraryManager, IUserManager userManager, ILogger logger)
+        {
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+            _logger = logger;
+        }
+
+        /// <summary>
+        /// Runs the specified progress.
+        /// </summary>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            var allItems = _libraryManager.RootFolder.RecursiveChildren
+                .Where(i => !(i is IHasMusicGenres) && !(i is Game))
+                .ToList();
+
+            var userLibraries = _userManager.Users
+                .Select(i => new Tuple<Guid, List<BaseItem>>(i.Id, i.RootFolder.GetRecursiveChildren(i).Where(m => !(m is IHasMusicGenres) && !(m is Game)).ToList()))
+                .ToList();
+
+            var allLibraryItems = allItems;
+
+            var masterDictionary = new Dictionary<string, Dictionary<Guid, Dictionary<string, int>>>(StringComparer.OrdinalIgnoreCase);
+
+            // Populate counts of items
+            SetItemCounts(null, allLibraryItems, masterDictionary);
+
+            progress.Report(2);
+
+            var numComplete = 0;
+
+            foreach (var lib in userLibraries)
+            {
+                SetItemCounts(lib.Item1, lib.Item2, masterDictionary);
+
+                numComplete++;
+                double percent = numComplete;
+                percent /= userLibraries.Count;
+                percent *= 8;
+
+                progress.Report(percent);
+            }
+
+            progress.Report(10);
+
+            var names = masterDictionary.Keys.ToList();
+            numComplete = 0;
+
+            foreach (var name in names)
+            {
+                try
+                {
+                    await UpdateItemByNameCounts(name, cancellationToken, masterDictionary[name]).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error updating counts for {0}", ex, name);
+                }
+
+                numComplete++;
+                double percent = numComplete;
+                percent /= names.Count;
+                percent *= 90;
+
+                progress.Report(percent + 10);
+            }
+
+            progress.Report(100);
+        }
+
+        private async Task UpdateItemByNameCounts(string name, CancellationToken cancellationToken, Dictionary<Guid, Dictionary<string, int>> counts)
+        {
+            var itemByName = await _libraryManager.GetGenre(name, cancellationToken, true, true).ConfigureAwait(false);
+
+            foreach (var libraryId in counts.Keys.ToList())
+            {
+                var itemCounts = CountHelpers.GetCounts(counts[libraryId]);
+
+                if (libraryId == Guid.Empty)
+                {
+                    itemByName.ItemCounts = itemCounts;
+                }
+                else
+                {
+                    itemByName.UserItemCounts[libraryId] = itemCounts;
+                }
+            }
+        }
+
+        private void SetItemCounts(Guid? userId, IEnumerable<BaseItem> allItems, Dictionary<string, Dictionary<Guid, Dictionary<string, int>>> masterDictionary)
+        {
+            foreach (var media in allItems)
+            {
+                var names = media
+                    .Genres
+                    .Distinct(StringComparer.OrdinalIgnoreCase)
+                    .ToList();
+
+                CountHelpers.SetItemCounts(userId, media, names, masterDictionary);
+            }
+        }
+    }
+}

+ 45 - 0
MediaBrowser.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs

@@ -0,0 +1,45 @@
+using MediaBrowser.Controller.Library;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Library.Validators
+{
+    /// <summary>
+    /// Class MusicGenresPostScanTask
+    /// </summary>
+    public class MusicGenresPostScanTask : ILibraryPostScanTask
+    {
+        /// <summary>
+        /// The _library manager
+        /// </summary>
+        private readonly ILibraryManager _libraryManager;
+
+        /// <summary>
+        /// The _user manager
+        /// </summary>
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
+        /// </summary>
+        /// <param name="libraryManager">The library manager.</param>
+        /// <param name="userManager">The user manager.</param>
+        public MusicGenresPostScanTask(ILibraryManager libraryManager, IUserManager userManager)
+        {
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+        }
+
+        /// <summary>
+        /// Runs the specified progress.
+        /// </summary>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            return _libraryManager.ValidateMusicGenres(cancellationToken, progress);
+        }
+    }
+}

+ 135 - 0
MediaBrowser.Server.Implementations/Library/Validators/MusicGenresValidator.cs

@@ -0,0 +1,135 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Library.Validators
+{
+    class MusicGenresValidator
+    {
+        /// <summary>
+        /// The _library manager
+        /// </summary>
+        private readonly LibraryManager _libraryManager;
+
+        /// <summary>
+        /// The _user manager
+        /// </summary>
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// The _logger
+        /// </summary>
+        private readonly ILogger _logger;
+
+        public MusicGenresValidator(LibraryManager libraryManager, IUserManager userManager, ILogger logger)
+        {
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+            _logger = logger;
+        }
+
+        /// <summary>
+        /// Runs the specified progress.
+        /// </summary>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            var allItems = _libraryManager.RootFolder.RecursiveChildren
+                .Where(i => i is IHasMusicGenres)
+                .ToList();
+
+            var userLibraries = _userManager.Users
+                .Select(i => new Tuple<Guid, List<BaseItem>>(i.Id, i.RootFolder.GetRecursiveChildren(i).Where(m => m is IHasMusicGenres).ToList()))
+                .ToList();
+
+            var allLibraryItems = allItems;
+
+            var masterDictionary = new Dictionary<string, Dictionary<Guid, Dictionary<string, int>>>(StringComparer.OrdinalIgnoreCase);
+
+            // Populate counts of items
+            SetItemCounts(null, allLibraryItems, masterDictionary);
+
+            progress.Report(2);
+
+            var numComplete = 0;
+
+            foreach (var lib in userLibraries)
+            {
+                SetItemCounts(lib.Item1, lib.Item2, masterDictionary);
+
+                numComplete++;
+                double percent = numComplete;
+                percent /= userLibraries.Count;
+                percent *= 8;
+
+                progress.Report(percent);
+            }
+
+            progress.Report(10);
+
+            var names = masterDictionary.Keys.ToList();
+            numComplete = 0;
+
+            foreach (var name in names)
+            {
+                try
+                {
+                    await UpdateItemByNameCounts(name, cancellationToken, masterDictionary[name]).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error updating counts for {0}", ex, name);
+                }
+
+                numComplete++;
+                double percent = numComplete;
+                percent /= names.Count;
+                percent *= 90;
+
+                progress.Report(percent + 10);
+            }
+
+            progress.Report(100);
+        }
+
+        private async Task UpdateItemByNameCounts(string name, CancellationToken cancellationToken, Dictionary<Guid, Dictionary<string, int>> counts)
+        {
+            var itemByName = await _libraryManager.GetMusicGenre(name, cancellationToken, true, true).ConfigureAwait(false);
+
+            foreach (var libraryId in counts.Keys.ToList())
+            {
+                var itemCounts = CountHelpers.GetCounts(counts[libraryId]);
+
+                if (libraryId == Guid.Empty)
+                {
+                    itemByName.ItemCounts = itemCounts;
+                }
+                else
+                {
+                    itemByName.UserItemCounts[libraryId] = itemCounts;
+                }
+            }
+        }
+
+        private void SetItemCounts(Guid? userId, IEnumerable<BaseItem> allItems, Dictionary<string, Dictionary<Guid, Dictionary<string, int>>> masterDictionary)
+        {
+            foreach (var media in allItems)
+            {
+                var names = media
+                    .Genres
+                    .Distinct(StringComparer.OrdinalIgnoreCase)
+                    .ToList();
+
+                CountHelpers.SetItemCounts(userId, media, names, masterDictionary);
+            }
+        }
+    }
+}

+ 137 - 0
MediaBrowser.Server.Implementations/Library/Validators/PeoplePostScanTask.cs

@@ -0,0 +1,137 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Library.Validators
+{
+    class PeoplePostScanTask : ILibraryPostScanTask
+    {
+        /// <summary>
+        /// The _library manager
+        /// </summary>
+        private readonly ILibraryManager _libraryManager;
+
+        /// <summary>
+        /// The _user manager
+        /// </summary>
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// The _logger
+        /// </summary>
+        private readonly ILogger _logger;
+
+        public PeoplePostScanTask(ILibraryManager libraryManager, IUserManager userManager, ILogger logger)
+        {
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+            _logger = logger;
+        }
+
+        /// <summary>
+        /// Runs the specified progress.
+        /// </summary>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            var allItems = _libraryManager.RootFolder.RecursiveChildren.ToList();
+
+            var userLibraries = _userManager.Users
+                .Select(i => new Tuple<Guid, List<BaseItem>>(i.Id, i.RootFolder.GetRecursiveChildren(i).ToList()))
+                .ToList();
+
+            var allLibraryItems = allItems;
+
+            var masterDictionary = new Dictionary<string, Dictionary<Guid, Dictionary<string, int>>>(StringComparer.OrdinalIgnoreCase);
+
+            // Populate counts of items
+            SetItemCounts(null, allLibraryItems, masterDictionary);
+
+            progress.Report(2);
+
+            var numComplete = 0;
+
+            foreach (var lib in userLibraries)
+            {
+                cancellationToken.ThrowIfCancellationRequested();
+
+                SetItemCounts(lib.Item1, lib.Item2, masterDictionary);
+
+                numComplete++;
+                double percent = numComplete;
+                percent /= userLibraries.Count;
+                percent *= 8;
+
+                progress.Report(percent);
+            }
+
+            progress.Report(10);
+
+            var names = masterDictionary.Keys.ToList();
+            numComplete = 0;
+
+            foreach (var name in names)
+            {
+                cancellationToken.ThrowIfCancellationRequested();
+                
+                try
+                {
+                    await UpdateItemByNameCounts(name, masterDictionary[name]).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error updating counts for {0}", ex, name);
+                }
+
+                numComplete++;
+                double percent = numComplete;
+                percent /= names.Count;
+                percent *= 90;
+
+                progress.Report(percent + 10);
+            }
+
+            progress.Report(100);
+        }
+
+        private async Task UpdateItemByNameCounts(string name, Dictionary<Guid, Dictionary<string, int>> counts)
+        {
+            var itemByName = await _libraryManager.GetPerson(name).ConfigureAwait(false);
+
+            foreach (var libraryId in counts.Keys.ToList())
+            {
+                var itemCounts = CountHelpers.GetCounts(counts[libraryId]);
+
+                if (libraryId == Guid.Empty)
+                {
+                    itemByName.ItemCounts = itemCounts;
+                }
+                else
+                {
+                    itemByName.UserItemCounts[libraryId] = itemCounts;
+                }
+            }
+        }
+
+        private void SetItemCounts(Guid? userId, IEnumerable<BaseItem> allItems, Dictionary<string, Dictionary<Guid, Dictionary<string, int>>> masterDictionary)
+        {
+            foreach (var media in allItems)
+            {
+                var names = media
+                    .People.Select(i => i.Name)
+                    .Distinct(StringComparer.OrdinalIgnoreCase)
+                    .ToList();
+
+                CountHelpers.SetItemCounts(userId, media, names, masterDictionary);
+            }
+        }
+
+    }
+}

+ 45 - 0
MediaBrowser.Server.Implementations/Library/Validators/StudiosPostScanTask.cs

@@ -0,0 +1,45 @@
+using MediaBrowser.Controller.Library;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Library.Validators
+{
+    /// <summary>
+    /// Class MusicGenresPostScanTask
+    /// </summary>
+    public class StudiosPostScanTask : ILibraryPostScanTask
+    {
+        /// <summary>
+        /// The _library manager
+        /// </summary>
+        private readonly ILibraryManager _libraryManager;
+
+        /// <summary>
+        /// The _user manager
+        /// </summary>
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
+        /// </summary>
+        /// <param name="libraryManager">The library manager.</param>
+        /// <param name="userManager">The user manager.</param>
+        public StudiosPostScanTask(ILibraryManager libraryManager, IUserManager userManager)
+        {
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+        }
+
+        /// <summary>
+        /// Runs the specified progress.
+        /// </summary>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            return _libraryManager.ValidateStudios(cancellationToken, progress);
+        }
+    }
+}

+ 132 - 0
MediaBrowser.Server.Implementations/Library/Validators/StudiosValidator.cs

@@ -0,0 +1,132 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Library.Validators
+{
+    class StudiosValidator
+    {
+        /// <summary>
+        /// The _library manager
+        /// </summary>
+        private readonly LibraryManager _libraryManager;
+
+        /// <summary>
+        /// The _user manager
+        /// </summary>
+        private readonly IUserManager _userManager;
+
+        /// <summary>
+        /// The _logger
+        /// </summary>
+        private readonly ILogger _logger;
+
+        public StudiosValidator(LibraryManager libraryManager, IUserManager userManager, ILogger logger)
+        {
+            _libraryManager = libraryManager;
+            _userManager = userManager;
+            _logger = logger;
+        }
+
+        /// <summary>
+        /// Runs the specified progress.
+        /// </summary>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            var allItems = _libraryManager.RootFolder.RecursiveChildren.ToList();
+
+            var userLibraries = _userManager.Users
+                .Select(i => new Tuple<Guid, List<BaseItem>>(i.Id, i.RootFolder.GetRecursiveChildren(i).ToList()))
+                .ToList();
+
+            var allLibraryItems = allItems;
+
+            var masterDictionary = new Dictionary<string, Dictionary<Guid, Dictionary<string, int>>>(StringComparer.OrdinalIgnoreCase);
+
+            // Populate counts of items
+            SetItemCounts(null, allLibraryItems, masterDictionary);
+
+            progress.Report(2);
+
+            var numComplete = 0;
+
+            foreach (var lib in userLibraries)
+            {
+                SetItemCounts(lib.Item1, lib.Item2, masterDictionary);
+
+                numComplete++;
+                double percent = numComplete;
+                percent /= userLibraries.Count;
+                percent *= 8;
+
+                progress.Report(percent);
+            }
+
+            progress.Report(10);
+
+            var names = masterDictionary.Keys.ToList();
+            numComplete = 0;
+
+            foreach (var name in names)
+            {
+                try
+                {
+                    await UpdateItemByNameCounts(name, cancellationToken, masterDictionary[name]).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error updating counts for {0}", ex, name);
+                }
+
+                numComplete++;
+                double percent = numComplete;
+                percent /= names.Count;
+                percent *= 90;
+
+                progress.Report(percent + 10);
+            }
+
+            progress.Report(100);
+        }
+
+        private async Task UpdateItemByNameCounts(string name, CancellationToken cancellationToken, Dictionary<Guid, Dictionary<string, int>> counts)
+        {
+            var itemByName = await _libraryManager.GetStudio(name, cancellationToken, true, true).ConfigureAwait(false);
+
+            foreach (var libraryId in counts.Keys.ToList())
+            {
+                var itemCounts = CountHelpers.GetCounts(counts[libraryId]);
+
+                if (libraryId == Guid.Empty)
+                {
+                    itemByName.ItemCounts = itemCounts;
+                }
+                else
+                {
+                    itemByName.UserItemCounts[libraryId] = itemCounts;
+                }
+            }
+        }
+
+        private void SetItemCounts(Guid? userId, IEnumerable<BaseItem> allItems, Dictionary<string, Dictionary<Guid, Dictionary<string, int>>> masterDictionary)
+        {
+            foreach (var media in allItems)
+            {
+                var names = media
+                    .Studios
+                    .Distinct(StringComparer.OrdinalIgnoreCase)
+                    .ToList();
+
+                CountHelpers.SetItemCounts(userId, media, names, masterDictionary);
+            }
+        }
+    }
+}

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

@@ -146,6 +146,18 @@
     <Compile Include="Library\Resolvers\TV\SeriesResolver.cs" />
     <Compile Include="Library\Resolvers\VideoResolver.cs" />
     <Compile Include="Library\UserManager.cs" />
+    <Compile Include="Library\Validators\ArtistsPostScanTask.cs" />
+    <Compile Include="Library\Validators\ArtistsValidator.cs" />
+    <Compile Include="Library\Validators\CountHelpers.cs" />
+    <Compile Include="Library\Validators\GameGenresPostScanTask.cs" />
+    <Compile Include="Library\Validators\GameGenresValidator.cs" />
+    <Compile Include="Library\Validators\GenresPostScanTask.cs" />
+    <Compile Include="Library\Validators\GenresValidator.cs" />
+    <Compile Include="Library\Validators\MusicGenresPostScanTask.cs" />
+    <Compile Include="Library\Validators\MusicGenresValidator.cs" />
+    <Compile Include="Library\Validators\PeoplePostScanTask.cs" />
+    <Compile Include="Library\Validators\StudiosPostScanTask.cs" />
+    <Compile Include="Library\Validators\StudiosValidator.cs" />
     <Compile Include="Localization\LocalizationManager.cs" />
     <Compile Include="MediaEncoder\MediaEncoder.cs" />
     <Compile Include="Persistence\SqliteChapterRepository.cs" />
@@ -155,7 +167,6 @@
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Providers\ImageSaver.cs" />
     <Compile Include="Providers\ProviderManager.cs" />
-    <Compile Include="ScheduledTasks\ArtistValidationTask.cs" />
     <Compile Include="ScheduledTasks\PeopleValidationTask.cs" />
     <Compile Include="ScheduledTasks\ChapterImagesTask.cs" />
     <Compile Include="ScheduledTasks\PluginUpdateTask.cs" />

+ 5 - 1
MediaBrowser.Server.Implementations/Providers/ProviderManager.cs

@@ -189,7 +189,11 @@ namespace MediaBrowser.Server.Implementations.Providers
 
             cancellationToken.ThrowIfCancellationRequested();
 
-            _logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name ?? "--Unknown--");
+            // Don't clog up the log with these providers
+            if (!(provider is IDynamicInfoProvider))
+            {
+                _logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name ?? "--Unknown--");
+            }
 
             // This provides the ability to cancel just this one provider
             var innerCancellationTokenSource = new CancellationTokenSource();

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

@@ -1,81 +0,0 @@
-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 - 5
MediaBrowser.Server.Implementations/Session/SessionManager.cs

@@ -85,11 +85,6 @@ namespace MediaBrowser.Server.Implementations.Session
             get { return _activeConnections.Values.OrderByDescending(c => c.LastActivityDate).ToList(); }
         }
 
-        /// <summary>
-        /// The _true task result
-        /// </summary>
-        private readonly Task _trueTaskResult = Task.FromResult(true);
-
         /// <summary>
         /// Logs the user activity.
         /// </summary>
@@ -339,6 +334,7 @@ namespace MediaBrowser.Server.Implementations.Session
                 // If the client isn't able to report this, then we'll just have to make an assumption
                 data.PlayCount++;
                 data.Played = true;
+                data.PlaybackPositionTicks = 0;
             }
 
             await _userDataRepository.SaveUserData(user.Id, key, data, CancellationToken.None).ConfigureAwait(false);

+ 0 - 7
MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj

@@ -336,11 +336,6 @@
   <ItemGroup>
     <Resource Include="Resources\Images\audio.png" />
   </ItemGroup>
-  <ItemGroup>
-    <Resource Include="Resources\Images\starEmpty.png" />
-    <Resource Include="Resources\Images\starFull.png" />
-    <Resource Include="Resources\Images\starHalf.png" />
-  </ItemGroup>
   <ItemGroup>
     <Resource Include="Resources\Images\artist.png" />
   </ItemGroup>
@@ -358,8 +353,6 @@
   </ItemGroup>
   <ItemGroup>
     <Resource Include="Resources\Images\folder.jpg" />
-    <Resource Include="Resources\Images\mblogoblackfull.png" />
-    <Resource Include="Resources\Images\mblogowhitefull.png" />
   </ItemGroup>
   <ItemGroup>
     <BootstrapperPackage Include=".NETFramework,Version=v4.5">

BIN
MediaBrowser.ServerApplication/Resources/Images/mblogoblackfull.png


BIN
MediaBrowser.ServerApplication/Resources/Images/mblogowhitefull.png


BIN
MediaBrowser.ServerApplication/Resources/Images/starEmpty.png


BIN
MediaBrowser.ServerApplication/Resources/Images/starFull.png


BIN
MediaBrowser.ServerApplication/Resources/Images/starHalf.png


+ 0 - 138
MediaBrowser.WebDashboard/ApiClient.js

@@ -3171,144 +3171,6 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi
             });
         };
 
-        /**
-            Gets a variety of item counts that a person appears in
-        */
-        self.getPersonItemCounts = function (userId, name) {
-
-            if (!userId) {
-                throw new Error("null userId");
-            }
-
-            if (!name) {
-                throw new Error("null name");
-            }
-
-            var url = self.getUrl("Persons/" + self.encodeName(name) + "/Counts", {
-                userId: userId
-            });
-
-            return self.ajax({
-                type: "GET",
-                url: url,
-                dataType: "json"
-            });
-        };
-
-        /**
-            Gets a variety of item counts that a genre appears in
-        */
-        self.getGenreItemCounts = function (userId, name) {
-
-            if (!userId) {
-                throw new Error("null userId");
-            }
-
-            if (!name) {
-                throw new Error("null name");
-            }
-
-            var url = self.getUrl("Genres/" + self.encodeName(name) + "/Counts", {
-                userId: userId
-            });
-
-            return self.ajax({
-                type: "GET",
-                url: url,
-                dataType: "json"
-            });
-        };
-
-        self.getMusicGenreItemCounts = function (userId, name) {
-
-            if (!userId) {
-                throw new Error("null userId");
-            }
-
-            if (!name) {
-                throw new Error("null name");
-            }
-
-            var url = self.getUrl("MusicGenres/" + self.encodeName(name) + "/Counts", {
-                userId: userId
-            });
-
-            return self.ajax({
-                type: "GET",
-                url: url,
-                dataType: "json"
-            });
-        };
-
-        self.getGameGenreItemCounts = function (userId, name) {
-
-            if (!userId) {
-                throw new Error("null userId");
-            }
-
-            if (!name) {
-                throw new Error("null name");
-            }
-
-            var url = self.getUrl("GameGenres/" + self.encodeName(name) + "/Counts", {
-                userId: userId
-            });
-
-            return self.ajax({
-                type: "GET",
-                url: url,
-                dataType: "json"
-            });
-        };
-
-        /**
-            Gets a variety of item counts that an artist appears in
-        */
-        self.getArtistItemCounts = function (userId, name) {
-
-            if (!userId) {
-                throw new Error("null userId");
-            }
-
-            if (!name) {
-                throw new Error("null name");
-            }
-
-            var url = self.getUrl("Artists/" + self.encodeName(name) + "/Counts", {
-                userId: userId
-            });
-
-            return self.ajax({
-                type: "GET",
-                url: url,
-                dataType: "json"
-            });
-        };
-
-        /**
-            Gets a variety of item counts that a studio appears in
-        */
-        self.getStudioItemCounts = function (userId, name) {
-
-            if (!userId) {
-                throw new Error("null userId");
-            }
-
-            if (!name) {
-                throw new Error("null name");
-            }
-
-            var url = self.getUrl("Studios/" + self.encodeName(name) + "/Counts", {
-                userId: userId
-            });
-
-            return self.ajax({
-                type: "GET",
-                url: url,
-                dataType: "json"
-            });
-        };
-
         /**
          * Clears a user's personal rating for an item
          * @param {String} userId

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

+ 3 - 0
MediaBrowser.sln

@@ -237,4 +237,7 @@ Global
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
 	EndGlobalSection
+	GlobalSection(Performance) = preSolution
+		HasPerformanceSessions = true
+	EndGlobalSection
 EndGlobal