Przeglądaj źródła

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

Luke Pulverenti 11 lat temu
rodzic
commit
740a10a4e3
63 zmienionych plików z 1923 dodań i 971 usunięć
  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