diff --git a/MediaBrowser.Controller/Dto/DtoBuilder.cs b/MediaBrowser.Controller/Dto/DtoBuilder.cs index 9256acd6e1..2631488a58 100644 --- a/MediaBrowser.Controller/Dto/DtoBuilder.cs +++ b/MediaBrowser.Controller/Dto/DtoBuilder.cs @@ -427,7 +427,7 @@ namespace MediaBrowser.Controller.Dto { dto.Album = audio.Album; dto.AlbumArtist = audio.AlbumArtist; - dto.Artist = audio.Artist; + dto.Artists = audio.Artists; } } diff --git a/MediaBrowser.Controller/Entities/Audio/Artist.cs b/MediaBrowser.Controller/Entities/Audio/Artist.cs new file mode 100644 index 0000000000..dcd6af92d3 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Audio/Artist.cs @@ -0,0 +1,18 @@ + +namespace MediaBrowser.Controller.Entities.Audio +{ + /// + /// Class Artist + /// + public class Artist : BaseItem + { + /// + /// Gets the user data key. + /// + /// System.String. + public override string GetUserDataKey() + { + return Name; + } + } +} diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index d629ec4d77..9deb8241de 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -1,5 +1,7 @@ using MediaBrowser.Model.Entities; +using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.Serialization; namespace MediaBrowser.Controller.Entities.Audio @@ -14,7 +16,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// /// The media streams. public List MediaStreams { get; set; } - + /// /// Override this to true if class should be grouped under a container in indicies /// The container class should be defined via IndexContainer @@ -51,7 +53,8 @@ namespace MediaBrowser.Controller.Entities.Audio /// Gets or sets the artist. /// /// The artist. - public string Artist { get; set; } + public List Artists { get; set; } + /// /// Gets or sets the album. /// @@ -75,6 +78,32 @@ namespace MediaBrowser.Controller.Entities.Audio } } + /// + /// Initializes a new instance of the class. + /// + public Audio() + { + Artists = new List(); + } + + /// + /// Adds the artist. + /// + /// The name. + /// name + public void AddArtist(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException("name"); + } + + if (!Artists.Contains(name, StringComparer.OrdinalIgnoreCase)) + { + Artists.Add(name); + } + } + /// /// Creates the name of the sort. /// diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 2b924ea884..14851ec430 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -127,7 +127,7 @@ namespace MediaBrowser.Controller.Entities /// IEnumerable{BaseItem}. protected IEnumerable GetIndexByPerformer(User user) { - return GetIndexByPerson(user, new List { PersonType.Actor, PersonType.MusicArtist, PersonType.GuestStar }, LocalizedStrings.Instance.GetString("PerformerDispPref")); + return GetIndexByPerson(user, new List { PersonType.Actor, PersonType.GuestStar }, true, LocalizedStrings.Instance.GetString("PerformerDispPref")); } /// @@ -137,7 +137,7 @@ namespace MediaBrowser.Controller.Entities /// IEnumerable{BaseItem}. protected IEnumerable GetIndexByDirector(User user) { - return GetIndexByPerson(user, new List { PersonType.Director }, LocalizedStrings.Instance.GetString("DirectorDispPref")); + return GetIndexByPerson(user, new List { PersonType.Director }, false, LocalizedStrings.Instance.GetString("DirectorDispPref")); } /// @@ -145,9 +145,10 @@ namespace MediaBrowser.Controller.Entities /// /// The user. /// The person types we should match on + /// if set to true [include audio]. /// Name of the index. /// IEnumerable{BaseItem}. - protected IEnumerable GetIndexByPerson(User user, List personTypes, string indexName) + private IEnumerable GetIndexByPerson(User user, List personTypes, bool includeAudio, string indexName) { // Even though this implementation means multiple iterations over the target list - it allows us to defer @@ -158,9 +159,12 @@ namespace MediaBrowser.Controller.Entities var currentIndexName = indexName; var us = this; - var candidates = RecursiveChildren.Where(i => i.IncludeInIndex && i.AllPeople != null).ToList(); + var recursiveChildren = GetRecursiveChildren(user).Where(i => i.IncludeInIndex).ToList(); - return candidates.AsParallel().SelectMany(i => i.AllPeople.Where(p => personTypes.Contains(p.Type)) + // Get the candidates, but handle audio separately + var candidates = recursiveChildren.Where(i => i.AllPeople != null && !(i is Audio.Audio)).ToList(); + + var indexFolders = candidates.AsParallel().SelectMany(i => i.AllPeople.Where(p => personTypes.Contains(p.Type)) .Select(a => a.Name)) .Distinct() .Select(i => @@ -183,8 +187,38 @@ namespace MediaBrowser.Controller.Entities .Where(i => i != null) .Select(a => new IndexFolder(us, a, candidates.Where(i => i.AllPeople.Any(p => personTypes.Contains(p.Type) && p.Name.Equals(a.Name, StringComparison.OrdinalIgnoreCase)) - ), currentIndexName)); + ), currentIndexName)).AsEnumerable(); + + if (includeAudio) + { + var songs = recursiveChildren.OfType().ToList(); + + indexFolders = songs.SelectMany(i => i.Artists) + .Distinct() + .Select(i => + { + try + { + return LibraryManager.GetArtist(i).Result; + } + catch (IOException ex) + { + Logger.ErrorException("Error getting artist {0}", ex, i); + return null; + } + catch (AggregateException ex) + { + Logger.ErrorException("Error getting artist {0}", ex, i); + return null; + } + }) + .Where(i => i != null) + .Select(a => new IndexFolder(us, a, + songs.Where(i => i.Artists.Contains(a.Name, StringComparer.OrdinalIgnoreCase) + ), currentIndexName)).Concat(indexFolders); + } + return indexFolders; } } @@ -201,7 +235,7 @@ namespace MediaBrowser.Controller.Entities { var indexName = LocalizedStrings.Instance.GetString("StudioDispPref"); - var candidates = RecursiveChildren.Where(i => i.IncludeInIndex && i.Studios != null).ToList(); + var candidates = GetRecursiveChildren(user).Where(i => i.IncludeInIndex && i.Studios != null).ToList(); return candidates.AsParallel().SelectMany(i => i.Studios) .Distinct() @@ -241,7 +275,7 @@ namespace MediaBrowser.Controller.Entities var indexName = LocalizedStrings.Instance.GetString("GenreDispPref"); //we need a copy of this so we don't double-recurse - var candidates = RecursiveChildren.Where(i => i.IncludeInIndex && i.Genres != null).ToList(); + var candidates = GetRecursiveChildren(user).Where(i => i.IncludeInIndex && i.Genres != null).ToList(); return candidates.AsParallel().SelectMany(i => i.Genres) .Distinct() @@ -282,7 +316,7 @@ namespace MediaBrowser.Controller.Entities var indexName = LocalizedStrings.Instance.GetString("YearDispPref"); //we need a copy of this so we don't double-recurse - var candidates = RecursiveChildren.Where(i => i.IncludeInIndex && i.ProductionYear.HasValue).ToList(); + var candidates = GetRecursiveChildren(user).Where(i => i.IncludeInIndex && i.ProductionYear.HasValue).ToList(); return candidates.AsParallel().Select(i => i.ProductionYear.Value) .Distinct() diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs index 6b17b5d798..1898f862cc 100644 --- a/MediaBrowser.Controller/IServerApplicationPaths.cs +++ b/MediaBrowser.Controller/IServerApplicationPaths.cs @@ -40,6 +40,12 @@ namespace MediaBrowser.Controller /// The genre path. string GenrePath { get; } + /// + /// Gets the artists path. + /// + /// The artists path. + string ArtistsPath { get; } + /// /// Gets the path to the Studio directory /// diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 617a6c8182..86fd25e661 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -1,4 +1,5 @@ using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Sorting; @@ -65,6 +66,14 @@ namespace MediaBrowser.Controller.Library /// Task{Person}. Task GetPerson(string name, bool allowSlowProviders = false); + /// + /// Gets the artist. + /// + /// The name. + /// if set to true [allow slow providers]. + /// Task{Artist}. + Task GetArtist(string name, bool allowSlowProviders = false); + /// /// Gets a Studio /// diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 33b0d3cb73..926639d0c7 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -71,6 +71,7 @@ + diff --git a/MediaBrowser.Controller/Providers/MediaInfo/FFProbeAudioInfoProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/FFProbeAudioInfoProvider.cs index 542b8eed85..56e85eb1f0 100644 --- a/MediaBrowser.Controller/Providers/MediaInfo/FFProbeAudioInfoProvider.cs +++ b/MediaBrowser.Controller/Providers/MediaInfo/FFProbeAudioInfoProvider.cs @@ -96,11 +96,7 @@ namespace MediaBrowser.Controller.Providers.MediaInfo if (!string.IsNullOrWhiteSpace(composer)) { - // Only use the comma as a delimeter if there are no slashes or pipes. - // We want to be careful not to split names that have commas in them - var delimeter = composer.IndexOf('/') == -1 && composer.IndexOf('|') == -1 ? new[] { ',' } : new[] { '/', '|' }; - - foreach (var person in composer.Split(delimeter, StringSplitOptions.RemoveEmptyEntries)) + foreach (var person in Split(composer)) { var name = person.Trim(); @@ -112,12 +108,19 @@ namespace MediaBrowser.Controller.Providers.MediaInfo } audio.Album = GetDictionaryValue(tags, "album"); - audio.Artist = GetDictionaryValue(tags, "artist"); - if (!string.IsNullOrWhiteSpace(audio.Artist)) + var artists = GetDictionaryValue(tags, "artist"); + if (!string.IsNullOrWhiteSpace(artists)) { - // Add to people too - audio.AddPerson(new PersonInfo {Name = audio.Artist, Type = PersonType.MusicArtist}); + foreach (var artist in Split(artists)) + { + var name = artist.Trim(); + + if (!string.IsNullOrEmpty(name)) + { + audio.AddArtist(name); + } + } } // Several different forms of albumartist @@ -150,6 +153,20 @@ namespace MediaBrowser.Controller.Providers.MediaInfo FetchStudios(audio, tags, "publisher"); } + /// + /// Splits the specified val. + /// + /// The val. + /// System.String[][]. + private string[] Split(string val) + { + // Only use the comma as a delimeter if there are no slashes or pipes. + // We want to be careful not to split names that have commas in them + var delimeter = val.IndexOf('/') == -1 && val.IndexOf('|') == -1 ? new[] { ',' } : new[] { '/', '|' }; + + return val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries); + } + /// /// Gets the studios from the tags collection /// diff --git a/MediaBrowser.Controller/Providers/Music/FanArtAlbumProvider.cs b/MediaBrowser.Controller/Providers/Music/FanArtAlbumProvider.cs index 72169f245e..f850722d1a 100644 --- a/MediaBrowser.Controller/Providers/Music/FanArtAlbumProvider.cs +++ b/MediaBrowser.Controller/Providers/Music/FanArtAlbumProvider.cs @@ -55,14 +55,7 @@ namespace MediaBrowser.Controller.Providers.Music var artist = (MusicArtist)item.Parent; var cover = artist.AlbumCovers != null ? GetValueOrDefault(artist.AlbumCovers, mbid, null) : null; - if (cover == null) - { - // Not there - maybe it is new since artist last refreshed so refresh it and try again - await artist.RefreshMetadata(cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - cover = artist.AlbumCovers != null ? GetValueOrDefault(artist.AlbumCovers, mbid, null) : null; - } if (cover == null) { Logger.Warn("Unable to find cover art for {0}", item.Name); diff --git a/MediaBrowser.Model/DTO/BaseItemDto.cs b/MediaBrowser.Model/DTO/BaseItemDto.cs index 645814fc84..db5949c292 100644 --- a/MediaBrowser.Model/DTO/BaseItemDto.cs +++ b/MediaBrowser.Model/DTO/BaseItemDto.cs @@ -268,7 +268,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the artist. /// /// The artist. - public string Artist { get; set; } + public List Artists { get; set; } /// /// Gets or sets the album. diff --git a/MediaBrowser.Model/Entities/PersonType.cs b/MediaBrowser.Model/Entities/PersonType.cs index 6c8352d62f..ee80b1496d 100644 --- a/MediaBrowser.Model/Entities/PersonType.cs +++ b/MediaBrowser.Model/Entities/PersonType.cs @@ -23,10 +23,6 @@ namespace MediaBrowser.Model.Entities /// public const string Writer = "Writer"; /// - /// The music artist - /// - public const string MusicArtist = "MusicArtist"; - /// /// The guest star /// public const string GuestStar = "GuestStar"; diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs index 1d5613501d..4d66d14227 100644 --- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs +++ b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs @@ -3,6 +3,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; @@ -551,6 +552,17 @@ namespace MediaBrowser.Server.Implementations.Library return GetImagesByNameItem(ConfigurationManager.ApplicationPaths.GenrePath, name, CancellationToken.None, allowSlowProviders); } + /// + /// Gets a Genre + /// + /// The name. + /// if set to true [allow slow providers]. + /// Task{Genre}. + public Task GetArtist(string name, bool allowSlowProviders = false) + { + return GetImagesByNameItem(ConfigurationManager.ApplicationPaths.ArtistsPath, name, CancellationToken.None, allowSlowProviders); + } + /// /// The us culture /// diff --git a/MediaBrowser.Server.Implementations/ServerApplicationPaths.cs b/MediaBrowser.Server.Implementations/ServerApplicationPaths.cs index 25d9cf5330..a723eb38e0 100644 --- a/MediaBrowser.Server.Implementations/ServerApplicationPaths.cs +++ b/MediaBrowser.Server.Implementations/ServerApplicationPaths.cs @@ -18,6 +18,9 @@ namespace MediaBrowser.Server.Implementations { } #else +/// +/// Initializes a new instance of the class. +/// public ServerApplicationPaths() : base(false) { @@ -345,5 +348,30 @@ namespace MediaBrowser.Server.Implementations return _downloadedImagesDataPath; } } + + /// + /// The _music artists path + /// + private string _musicArtistsPath; + /// + /// Gets the artists path. + /// + /// The artists path. + public string ArtistsPath + { + get + { + if (_musicArtistsPath == null) + { + _musicArtistsPath = Path.Combine(ImagesByNamePath, "Artists"); + if (!Directory.Exists(_musicArtistsPath)) + { + Directory.CreateDirectory(_musicArtistsPath); + } + } + + return _musicArtistsPath; + } + } } } diff --git a/MediaBrowser.Server.Implementations/Sorting/ArtistComparer.cs b/MediaBrowser.Server.Implementations/Sorting/ArtistComparer.cs index c34f096a2a..d31ee87906 100644 --- a/MediaBrowser.Server.Implementations/Sorting/ArtistComparer.cs +++ b/MediaBrowser.Server.Implementations/Sorting/ArtistComparer.cs @@ -3,6 +3,7 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; using System; +using System.Linq; namespace MediaBrowser.Server.Implementations.Sorting { @@ -31,7 +32,7 @@ namespace MediaBrowser.Server.Implementations.Sorting { var audio = x as Audio; - return audio == null ? string.Empty : audio.Artist; + return audio == null ? string.Empty : audio.Artists.OrderBy(i => i).FirstOrDefault() ?? string.Empty; } ///