Ver código fonte

Merge pull request #7514 from Shadowghost/music-extend

Claus Vium 2 anos atrás
pai
commit
b137d0cc2b

+ 23 - 0
Emby.Naming/Common/NamingOptions.cs

@@ -181,6 +181,24 @@ namespace Emby.Naming.Common
                 "volume"
             };
 
+            ArtistSubfolders = new[]
+            {
+                "albums",
+                "broadcasts",
+                "bootlegs",
+                "compilations",
+                "dj-mixes",
+                "eps",
+                "live",
+                "mixtapes",
+                "others",
+                "remixes",
+                "singles",
+                "soundtracks",
+                "spokenwords",
+                "streets"
+            };
+
             AudioFileExtensions = new[]
             {
                 ".669",
@@ -744,6 +762,11 @@ namespace Emby.Naming.Common
         /// </summary>
         public string[] AlbumStackingPrefixes { get; set; }
 
+        /// <summary>
+        /// Gets or sets list of artist subfolders.
+        /// </summary>
+        public string[] ArtistSubfolders { get; set; }
+
         /// <summary>
         /// Gets or sets list of subtitle file extensions.
         /// </summary>

+ 18 - 7
Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs

@@ -2,6 +2,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
@@ -18,7 +19,7 @@ using Microsoft.Extensions.Logging;
 namespace Emby.Server.Implementations.Library.Resolvers.Audio
 {
     /// <summary>
-    /// Class MusicAlbumResolver.
+    /// The music album resolver.
     /// </summary>
     public class MusicAlbumResolver : ItemResolver<MusicAlbum>
     {
@@ -82,7 +83,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         /// </summary>
         /// <param name="path">The path to check.</param>
         /// <param name="directoryService">The directory service.</param>
-        /// <returns><c>true</c> if the provided path points to a music album, <c>false</c> otherwise.</returns>
+        /// <returns><c>true</c> if the provided path points to a music album; otherwise, <c>false</c>.</returns>
         public bool IsMusicAlbum(string path, IDirectoryService directoryService)
         {
             return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService);
@@ -95,10 +96,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         /// <returns><c>true</c> if [is music album] [the specified args]; otherwise, <c>false</c>.</returns>
         private bool IsMusicAlbum(ItemResolveArgs args)
         {
-            // Args points to an album if parent is an Artist folder or it directly contains music
             if (args.IsDirectory)
             {
-                // if (args.Parent is MusicArtist) return true;  // saves us from testing children twice
+                // If args is a artist subfolder it's not a music album
+                foreach (var subfolder in _namingOptions.ArtistSubfolders)
+                {
+                    if (Path.GetDirectoryName(args.Path.AsSpan()).Equals(subfolder, StringComparison.OrdinalIgnoreCase))
+                    {
+                        _logger.LogDebug("Found release folder: {Path}", args.Path);
+                        return false;
+                    }
+                }
+
+                // If args contains music it's a music album
                 if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService))
                 {
                     return true;
@@ -111,22 +121,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         /// <summary>
         /// Determine if the supplied list contains what we should consider music.
         /// </summary>
+        /// <returns><c>true</c> if the provided path list contains music; otherwise, <c>false</c>.</returns>
         private bool ContainsMusic(
             ICollection<FileSystemMetadata> list,
             bool allowSubfolders,
             IDirectoryService directoryService)
         {
-            // check for audio files before digging down into directories
+            // Check for audio files before digging down into directories
             var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && AudioFileParser.IsAudioFile(fileSystemInfo.FullName, _namingOptions));
             if (foundAudioFile)
             {
-                // at least one audio file exists
+                // At least one audio file exists
                 return true;
             }
 
             if (!allowSubfolders)
             {
-                // not music since no audio file exists and we're not looking into subfolders
+                // Not music since no audio file exists and we're not looking into subfolders
                 return false;
             }
 

+ 19 - 9
Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs

@@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging;
 namespace Emby.Server.Implementations.Library.Resolvers.Audio
 {
     /// <summary>
-    /// Class MusicArtistResolver.
+    /// The music artist resolver.
     /// </summary>
     public class MusicArtistResolver : ItemResolver<MusicArtist>
     {
@@ -23,8 +23,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         /// <summary>
         /// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
         /// </summary>
-        /// <param name="logger">The logger for the created <see cref="MusicAlbumResolver"/> instances.</param>
-        /// <param name="namingOptions">The naming options.</param>
+        /// <param name="logger">Instance of the <see cref="MusicAlbumResolver"/> interface.</param>
+        /// <param name="namingOptions">The <see cref="NamingOptions"/>.</param>
         public MusicArtistResolver(
             ILogger<MusicAlbumResolver> logger,
             NamingOptions namingOptions)
@@ -40,10 +40,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         public override ResolverPriority Priority => ResolverPriority.Second;
 
         /// <summary>
-        /// Resolves the specified args.
+        /// Resolves the specified resolver arguments.
         /// </summary>
-        /// <param name="args">The args.</param>
-        /// <returns>MusicArtist.</returns>
+        /// <param name="args">The resolver arguments.</param>
+        /// <returns>A <see cref="MusicArtist"/>.</returns>
         protected override MusicArtist Resolve(ItemResolveArgs args)
         {
             if (!args.IsDirectory)
@@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
 
             var isMusicMediaFolder = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase);
 
-            // If there's a collection type and it's not music, it can't be a series
+            // If there's a collection type and it's not music, it can't be a music artist
             if (!isMusicMediaFolder)
             {
                 return null;
@@ -82,14 +82,24 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
 
             var albumResolver = new MusicAlbumResolver(_logger, _namingOptions);
 
-            // If we contain an album assume we are an artist folder
             var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
 
             var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
             {
+                // If we contain a artist subfolder assume we are an artist folder
+                foreach (var subfolder in _namingOptions.ArtistSubfolders)
+                {
+                    if (fileSystemInfo.Name.Equals(subfolder, StringComparison.OrdinalIgnoreCase))
+                    {
+                        // Stop once we see an artist subfolder
+                        state.Stop();
+                    }
+                }
+
+                // If we contain a music album assume we are an artist folder
                 if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, directoryService))
                 {
-                    // stop once we see a music album
+                    // Stop once we see a music album
                     state.Stop();
                 }
             });

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

@@ -54,7 +54,7 @@ namespace MediaBrowser.Controller.Entities.Audio
         public string AlbumArtist => AlbumArtists.FirstOrDefault();
 
         [JsonIgnore]
-        public override bool SupportsPeople => false;
+        public override bool SupportsPeople => true;
 
         /// <summary>
         /// Gets the tracks.

+ 3 - 3
MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs

@@ -18,9 +18,9 @@ namespace MediaBrowser.Controller.Providers
         /// Fetches the metadata asynchronously.
         /// </summary>
         /// <param name="item">The item.</param>
-        /// <param name="options">The options.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{ItemUpdateType}.</returns>
+        /// <param name="options">The <see cref="MetadataRefreshOptions"/>.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
+        /// <returns>A <see cref="Task"/> fetching the <see cref="ItemUpdateType"/>.</returns>
         Task<ItemUpdateType> FetchAsync(TItemType item, MetadataRefreshOptions options, CancellationToken cancellationToken);
     }
 }

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

@@ -22,6 +22,7 @@
     <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
     <PackageReference Include="OptimizedPriorityQueue" Version="5.1.0" />
     <PackageReference Include="PlaylistsNET" Version="1.2.1" />
+    <PackageReference Include="TagLibSharp" Version="2.3.0" />
     <PackageReference Include="TMDbLib" Version="1.9.2" />
   </ItemGroup>
 

+ 215 - 0
MediaBrowser.Providers/MediaInfo/AudioFileProber.cs

@@ -0,0 +1,215 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.MediaInfo;
+using TagLib;
+
+namespace MediaBrowser.Providers.MediaInfo
+{
+    /// <summary>
+    /// Probes audio files for metadata.
+    /// </summary>
+    public class AudioFileProber
+    {
+        private readonly IMediaEncoder _mediaEncoder;
+        private readonly IItemRepository _itemRepo;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IMediaSourceManager _mediaSourceManager;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AudioFileProber"/> class.
+        /// </summary>
+        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+        /// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        public AudioFileProber(
+            IMediaSourceManager mediaSourceManager,
+            IMediaEncoder mediaEncoder,
+            IItemRepository itemRepo,
+            ILibraryManager libraryManager)
+        {
+            _mediaEncoder = mediaEncoder;
+            _itemRepo = itemRepo;
+            _libraryManager = libraryManager;
+            _mediaSourceManager = mediaSourceManager;
+        }
+
+        /// <summary>
+        /// Probes the specified item for metadata.
+        /// </summary>
+        /// <param name="item">The item to probe.</param>
+        /// <param name="options">The <see cref="MetadataRefreshOptions"/>.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
+        /// <typeparam name="T">The type of item to resolve.</typeparam>
+        /// <returns>A <see cref="Task"/> probing the item for metadata.</returns>
+        public async Task<ItemUpdateType> Probe<T>(
+            T item,
+            MetadataRefreshOptions options,
+            CancellationToken cancellationToken)
+            where T : Audio
+        {
+            var path = item.Path;
+            var protocol = item.PathProtocol ?? MediaProtocol.File;
+
+            if (!item.IsShortcut || options.EnableRemoteContentProbe)
+            {
+                if (item.IsShortcut)
+                {
+                    path = item.ShortcutPath;
+                    protocol = _mediaSourceManager.GetPathProtocol(path);
+                }
+
+                var result = await _mediaEncoder.GetMediaInfo(
+                    new MediaInfoRequest
+                    {
+                        MediaType = DlnaProfileType.Audio,
+                        MediaSource = new MediaSourceInfo
+                        {
+                            Path = path,
+                            Protocol = protocol
+                        }
+                    },
+                    cancellationToken).ConfigureAwait(false);
+
+                cancellationToken.ThrowIfCancellationRequested();
+
+                Fetch(item, result, cancellationToken);
+            }
+
+            return ItemUpdateType.MetadataImport;
+        }
+
+        /// <summary>
+        /// Fetches the specified audio.
+        /// </summary>
+        /// <param name="audio">The <see cref="Audio"/>.</param>
+        /// <param name="mediaInfo">The <see cref="Model.MediaInfo.MediaInfo"/>.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
+        protected void Fetch(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, CancellationToken cancellationToken)
+        {
+            audio.Container = mediaInfo.Container;
+            audio.TotalBitrate = mediaInfo.Bitrate;
+
+            audio.RunTimeTicks = mediaInfo.RunTimeTicks;
+            audio.Size = mediaInfo.Size;
+
+            FetchDataFromTags(audio);
+
+            _itemRepo.SaveMediaStreams(audio.Id, mediaInfo.MediaStreams, cancellationToken);
+        }
+
+        /// <summary>
+        /// Fetches data from the tags.
+        /// </summary>
+        /// <param name="audio">The <see cref="Audio"/>.</param>
+        private void FetchDataFromTags(Audio audio)
+        {
+            var file = TagLib.File.Create(audio.Path);
+            var tagTypes = file.TagTypesOnDisk;
+            Tag? tags = null;
+
+            if (tagTypes.HasFlag(TagTypes.Id3v2))
+            {
+                tags = file.GetTag(TagTypes.Id3v2);
+            }
+            else if (tagTypes.HasFlag(TagTypes.Ape))
+            {
+                tags = file.GetTag(TagTypes.Ape);
+            }
+            else if (tagTypes.HasFlag(TagTypes.FlacMetadata))
+            {
+                tags = file.GetTag(TagTypes.FlacMetadata);
+            }
+            else if (tagTypes.HasFlag(TagTypes.Apple))
+            {
+                tags = file.GetTag(TagTypes.Apple);
+            }
+            else if (tagTypes.HasFlag(TagTypes.Xiph))
+            {
+                tags = file.GetTag(TagTypes.Xiph);
+            }
+            else if (tagTypes.HasFlag(TagTypes.AudibleMetadata))
+            {
+                tags = file.GetTag(TagTypes.AudibleMetadata);
+            }
+            else if (tagTypes.HasFlag(TagTypes.Id3v1))
+            {
+                tags = file.GetTag(TagTypes.Id3v1);
+            }
+
+            if (tags != null)
+            {
+                if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
+                {
+                    var people = new List<PersonInfo>();
+                    var albumArtists = tags.AlbumArtists;
+                    foreach (var albumArtist in albumArtists)
+                    {
+                        PeopleHelper.AddPerson(people, new PersonInfo
+                        {
+                            Name = albumArtist,
+                            Type = "AlbumArtist"
+                        });
+                    }
+
+                    var performers = tags.Performers;
+                    foreach (var performer in performers)
+                    {
+                        PeopleHelper.AddPerson(people, new PersonInfo
+                        {
+                            Name = performer,
+                            Type = "Artist"
+                        });
+                    }
+
+                    foreach (var composer in tags.Composers)
+                    {
+                        PeopleHelper.AddPerson(people, new PersonInfo
+                        {
+                            Name = composer,
+                            Type = "Composer"
+                        });
+                    }
+
+                    _libraryManager.UpdatePeople(audio, people);
+                    audio.Artists = performers;
+                    audio.AlbumArtists = albumArtists;
+                }
+
+                audio.Name = tags.Title;
+                audio.Album = tags.Album;
+                audio.IndexNumber = Convert.ToInt32(tags.Track);
+                audio.ParentIndexNumber = Convert.ToInt32(tags.Disc);
+                if (tags.Year != 0)
+                {
+                    var year = Convert.ToInt32(tags.Year);
+                    audio.ProductionYear = year;
+                    audio.PremiereDate = new DateTime(year, 01, 01);
+                }
+
+                if (!audio.LockedFields.Contains(MetadataField.Genres))
+                {
+                    audio.Genres = tags.Genres.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
+                }
+
+                audio.SetProviderId(MetadataProvider.MusicBrainzArtist, tags.MusicBrainzArtistId);
+                audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, tags.MusicBrainzReleaseArtistId);
+                audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, tags.MusicBrainzReleaseId);
+                audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, tags.MusicBrainzReleaseGroupId);
+                audio.SetProviderId(MetadataProvider.MusicBrainzTrack, tags.MusicBrainzTrackId);
+            }
+        }
+    }
+}

+ 0 - 172
MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs

@@ -1,172 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.MediaInfo;
-
-namespace MediaBrowser.Providers.MediaInfo
-{
-    public class FFProbeAudioInfo
-    {
-        private readonly IMediaEncoder _mediaEncoder;
-        private readonly IItemRepository _itemRepo;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IMediaSourceManager _mediaSourceManager;
-
-        public FFProbeAudioInfo(
-            IMediaSourceManager mediaSourceManager,
-            IMediaEncoder mediaEncoder,
-            IItemRepository itemRepo,
-            ILibraryManager libraryManager)
-        {
-            _mediaEncoder = mediaEncoder;
-            _itemRepo = itemRepo;
-            _libraryManager = libraryManager;
-            _mediaSourceManager = mediaSourceManager;
-        }
-
-        public async Task<ItemUpdateType> Probe<T>(
-            T item,
-            MetadataRefreshOptions options,
-            CancellationToken cancellationToken)
-            where T : Audio
-        {
-            var path = item.Path;
-            var protocol = item.PathProtocol ?? MediaProtocol.File;
-
-            if (!item.IsShortcut || options.EnableRemoteContentProbe)
-            {
-                if (item.IsShortcut)
-                {
-                    path = item.ShortcutPath;
-                    protocol = _mediaSourceManager.GetPathProtocol(path);
-                }
-
-                var result = await _mediaEncoder.GetMediaInfo(
-                    new MediaInfoRequest
-                    {
-                        MediaType = DlnaProfileType.Audio,
-                        MediaSource = new MediaSourceInfo
-                        {
-                            Path = path,
-                            Protocol = protocol
-                        }
-                    },
-                    cancellationToken).ConfigureAwait(false);
-
-                cancellationToken.ThrowIfCancellationRequested();
-
-                Fetch(item, result, cancellationToken);
-            }
-
-            return ItemUpdateType.MetadataImport;
-        }
-
-        /// <summary>
-        /// Fetches the specified audio.
-        /// </summary>
-        /// <param name="audio">The audio.</param>
-        /// <param name="mediaInfo">The media information.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        protected void Fetch(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, CancellationToken cancellationToken)
-        {
-            audio.Container = mediaInfo.Container;
-            audio.TotalBitrate = mediaInfo.Bitrate;
-
-            audio.RunTimeTicks = mediaInfo.RunTimeTicks;
-            audio.Size = mediaInfo.Size;
-
-            // var extension = (Path.GetExtension(audio.Path) ?? string.Empty).TrimStart('.');
-            // audio.Container = extension;
-
-            FetchDataFromTags(audio, mediaInfo);
-
-            _itemRepo.SaveMediaStreams(audio.Id, mediaInfo.MediaStreams, cancellationToken);
-        }
-
-        /// <summary>
-        /// Fetches data from the tags dictionary.
-        /// </summary>
-        /// <param name="audio">The audio.</param>
-        /// <param name="data">The data.</param>
-        private void FetchDataFromTags(Audio audio, Model.MediaInfo.MediaInfo data)
-        {
-            // Only set Name if title was found in the dictionary
-            if (!string.IsNullOrEmpty(data.Name))
-            {
-                audio.Name = data.Name;
-            }
-
-            if (!string.IsNullOrEmpty(data.ForcedSortName))
-            {
-                audio.ForcedSortName = data.ForcedSortName;
-            }
-
-            if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
-            {
-                var people = new List<PersonInfo>();
-
-                foreach (var person in data.People)
-                {
-                    PeopleHelper.AddPerson(people, new PersonInfo
-                    {
-                        Name = person.Name,
-                        Type = person.Type,
-                        Role = person.Role
-                    });
-                }
-
-                _libraryManager.UpdatePeople(audio, people);
-            }
-
-            audio.Album = data.Album;
-            audio.Artists = data.Artists;
-            audio.AlbumArtists = data.AlbumArtists;
-            audio.IndexNumber = data.IndexNumber;
-            audio.ParentIndexNumber = data.ParentIndexNumber;
-            audio.ProductionYear = data.ProductionYear;
-            audio.PremiereDate = data.PremiereDate;
-
-            // If we don't have a ProductionYear try and get it from PremiereDate
-            if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue)
-            {
-                audio.ProductionYear = audio.PremiereDate.Value.ToLocalTime().Year;
-            }
-
-            if (!audio.LockedFields.Contains(MetadataField.Genres))
-            {
-                audio.Genres = Array.Empty<string>();
-
-                foreach (var genre in data.Genres)
-                {
-                    audio.AddGenre(genre);
-                }
-            }
-
-            if (!audio.LockedFields.Contains(MetadataField.Studios))
-            {
-                audio.SetStudios(data.Studios);
-            }
-
-            audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, data.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist));
-            audio.SetProviderId(MetadataProvider.MusicBrainzArtist, data.GetProviderId(MetadataProvider.MusicBrainzArtist));
-            audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, data.GetProviderId(MetadataProvider.MusicBrainzAlbum));
-            audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, data.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup));
-            audio.SetProviderId(MetadataProvider.MusicBrainzTrack, data.GetProviderId(MetadataProvider.MusicBrainzTrack));
-        }
-    }
-}

+ 52 - 10
MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs → MediaBrowser.Providers/MediaInfo/ProbeProvider.cs

@@ -1,7 +1,5 @@
 #nullable disable
 
-#pragma warning disable CS1591
-
 using System;
 using System.IO;
 using System.Linq;
@@ -27,7 +25,10 @@ using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Providers.MediaInfo
 {
-    public class FFProbeProvider : ICustomMetadataProvider<Episode>,
+    /// <summary>
+    /// The probe provider.
+    /// </summary>
+    public class ProbeProvider : ICustomMetadataProvider<Episode>,
         ICustomMetadataProvider<MusicVideo>,
         ICustomMetadataProvider<Movie>,
         ICustomMetadataProvider<Trailer>,
@@ -39,14 +40,30 @@ namespace MediaBrowser.Providers.MediaInfo
         IPreRefreshProvider,
         IHasItemChangeMonitor
     {
-        private readonly ILogger<FFProbeProvider> _logger;
+        private readonly ILogger<ProbeProvider> _logger;
         private readonly AudioResolver _audioResolver;
         private readonly SubtitleResolver _subtitleResolver;
         private readonly FFProbeVideoInfo _videoProber;
-        private readonly FFProbeAudioInfo _audioProber;
+        private readonly AudioFileProber _audioProber;
         private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
 
-        public FFProbeProvider(
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ProbeProvider"/> class.
+        /// </summary>
+        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+        /// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param>
+        /// <param name="blurayExaminer">Instance of the <see cref="IBlurayExaminer"/> interface.</param>
+        /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+        /// <param name="encodingManager">Instance of the <see cref="IEncodingManager"/> interface.</param>
+        /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+        /// <param name="subtitleManager">Instance of the <see cref="ISubtitleManager"/> interface.</param>
+        /// <param name="chapterManager">Instance of the <see cref="IChapterManager"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+        /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/>.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="namingOptions">The <see cref="NamingOptions"/>.</param>
+        public ProbeProvider(
             IMediaSourceManager mediaSourceManager,
             IMediaEncoder mediaEncoder,
             IItemRepository itemRepo,
@@ -61,7 +78,8 @@ namespace MediaBrowser.Providers.MediaInfo
             ILoggerFactory loggerFactory,
             NamingOptions namingOptions)
         {
-            _logger = loggerFactory.CreateLogger<FFProbeProvider>();
+            _logger = loggerFactory.CreateLogger<ProbeProvider>();
+            _audioProber = new AudioFileProber(mediaSourceManager, mediaEncoder, itemRepo, libraryManager);
             _audioResolver = new AudioResolver(loggerFactory.CreateLogger<AudioResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
             _subtitleResolver = new SubtitleResolver(loggerFactory.CreateLogger<SubtitleResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
             _videoProber = new FFProbeVideoInfo(
@@ -78,14 +96,15 @@ namespace MediaBrowser.Providers.MediaInfo
                 libraryManager,
                 _audioResolver,
                 _subtitleResolver);
-            _audioProber = new FFProbeAudioInfo(mediaSourceManager, mediaEncoder, itemRepo, libraryManager);
         }
 
-        public string Name => "ffprobe";
+        /// <inheritdoc />
+        public string Name => "Probe Provider";
 
-        // Run last
+        /// <inheritdoc />
         public int Order => 100;
 
+        /// <inheritdoc />
         public bool HasChanged(BaseItem item, IDirectoryService directoryService)
         {
             var video = item as Video;
@@ -127,41 +146,56 @@ namespace MediaBrowser.Providers.MediaInfo
             return false;
         }
 
+        /// <inheritdoc />
         public Task<ItemUpdateType> FetchAsync(Episode item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
             return FetchVideoInfo(item, options, cancellationToken);
         }
 
+        /// <inheritdoc />
         public Task<ItemUpdateType> FetchAsync(MusicVideo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
             return FetchVideoInfo(item, options, cancellationToken);
         }
 
+        /// <inheritdoc />
         public Task<ItemUpdateType> FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
             return FetchVideoInfo(item, options, cancellationToken);
         }
 
+        /// <inheritdoc />
         public Task<ItemUpdateType> FetchAsync(Trailer item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
             return FetchVideoInfo(item, options, cancellationToken);
         }
 
+        /// <inheritdoc />
         public Task<ItemUpdateType> FetchAsync(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
             return FetchVideoInfo(item, options, cancellationToken);
         }
 
+        /// <inheritdoc />
         public Task<ItemUpdateType> FetchAsync(Audio item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
             return FetchAudioInfo(item, options, cancellationToken);
         }
 
+        /// <inheritdoc />
         public Task<ItemUpdateType> FetchAsync(AudioBook item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
             return FetchAudioInfo(item, options, cancellationToken);
         }
 
+        /// <summary>
+        /// Fetches video information for an item.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="options">The <see cref="MetadataRefreshOptions"/>.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
+        /// <typeparam name="T">The type of item to resolve.</typeparam>
+        /// <returns>A <see cref="Task"/> fetching the <see cref="ItemUpdateType"/> for an item.</returns>
         public Task<ItemUpdateType> FetchVideoInfo<T>(T item, MetadataRefreshOptions options, CancellationToken cancellationToken)
             where T : Video
         {
@@ -208,6 +242,14 @@ namespace MediaBrowser.Providers.MediaInfo
                 .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i) && !i.StartsWith('#'));
         }
 
+        /// <summary>
+        /// Fetches audio information for an item.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="options">The <see cref="MetadataRefreshOptions"/>.</param>
+        /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
+        /// <typeparam name="T">The type of item to resolve.</typeparam>
+        /// <returns>A <see cref="Task"/> fetching the <see cref="ItemUpdateType"/> for an item.</returns>
         public Task<ItemUpdateType> FetchAudioInfo<T>(T item, MetadataRefreshOptions options, CancellationToken cancellationToken)
             where T : Audio
         {

+ 121 - 12
MediaBrowser.Providers/Music/AlbumMetadataService.cs

@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
 using System;
 using System.Collections.Generic;
 using System.Linq;
@@ -15,8 +13,19 @@ using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Providers.Music
 {
+    /// <summary>
+    /// The album metadata service.
+    /// </summary>
     public class AlbumMetadataService : MetadataService<MusicAlbum, AlbumInfo>
     {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AlbumMetadataService"/> class.
+        /// </summary>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+        /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
         public AlbumMetadataService(
             IServerConfigurationManager serverConfigurationManager,
             ILogger<AlbumMetadataService> logger,
@@ -61,40 +70,46 @@ namespace MediaBrowser.Providers.Music
 
                 var songs = children.Cast<Audio>().ToArray();
 
-                updateType |= SetAlbumArtistFromSongs(item, songs);
                 updateType |= SetArtistsFromSongs(item, songs);
+                updateType |= SetAlbumArtistFromSongs(item, songs);
+                updateType |= SetAlbumFromSongs(item, songs);
+                updateType |= SetPeople(item);
             }
 
             return updateType;
         }
 
-        private ItemUpdateType SetAlbumArtistFromSongs(MusicAlbum item, IEnumerable<Audio> songs)
+        private ItemUpdateType SetAlbumArtistFromSongs(MusicAlbum item, IReadOnlyList<Audio> songs)
         {
             var updateType = ItemUpdateType.None;
 
-            var artists = songs
+            var albumArtists = songs
                 .SelectMany(i => i.AlbumArtists)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
-                .OrderBy(i => i)
+                .GroupBy(i => i)
+                .OrderByDescending(g => g.Count())
+                .Select(g => g.Key)
                 .ToArray();
 
-            if (!item.AlbumArtists.SequenceEqual(artists, StringComparer.OrdinalIgnoreCase))
+            updateType |= SetProviderIdFromSongs(item, songs, MetadataProvider.MusicBrainzAlbumArtist);
+
+            if (!item.AlbumArtists.SequenceEqual(albumArtists, StringComparer.OrdinalIgnoreCase))
             {
-                item.AlbumArtists = artists;
+                item.AlbumArtists = albumArtists;
                 updateType |= ItemUpdateType.MetadataEdit;
             }
 
             return updateType;
         }
 
-        private ItemUpdateType SetArtistsFromSongs(MusicAlbum item, IEnumerable<Audio> songs)
+        private ItemUpdateType SetArtistsFromSongs(MusicAlbum item, IReadOnlyList<Audio> songs)
         {
             var updateType = ItemUpdateType.None;
 
             var artists = songs
                 .SelectMany(i => i.Artists)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
-                .OrderBy(i => i)
+                .GroupBy(i => i)
+                .OrderByDescending(g => g.Count())
+                .Select(g => g.Key)
                 .ToArray();
 
             if (!item.Artists.SequenceEqual(artists, StringComparer.OrdinalIgnoreCase))
@@ -106,6 +121,85 @@ namespace MediaBrowser.Providers.Music
             return updateType;
         }
 
+        private ItemUpdateType SetAlbumFromSongs(MusicAlbum item, IReadOnlyList<Audio> songs)
+        {
+            var updateType = ItemUpdateType.None;
+
+            updateType |= SetProviderIdFromSongs(item, songs, MetadataProvider.MusicBrainzAlbum);
+            updateType |= SetProviderIdFromSongs(item, songs, MetadataProvider.MusicBrainzReleaseGroup);
+
+            return updateType;
+        }
+
+        private ItemUpdateType SetProviderIdFromSongs(BaseItem item, IReadOnlyList<Audio> songs, MetadataProvider provider)
+        {
+            var ids = songs
+                .Select(i => i.GetProviderId(provider))
+                .GroupBy(i => i)
+                .OrderByDescending(g => g.Count())
+                .Select(g => g.Key)
+                .ToArray();
+
+            var id = item.GetProviderId(provider);
+            if (ids.Any())
+            {
+                var firstId = ids[0];
+                if (!string.IsNullOrEmpty(firstId)
+                    && (string.IsNullOrEmpty(id)
+                        || !id.Equals(firstId, StringComparison.OrdinalIgnoreCase)))
+                {
+                    item.SetProviderId(provider, firstId);
+                    return ItemUpdateType.MetadataEdit;
+                }
+            }
+            return ItemUpdateType.None;
+        }
+
+        private void SetProviderId(MusicAlbum sourceItem, MusicAlbum targetItem, MetadataProvider provider)
+        {
+            var source = sourceItem.GetProviderId(provider);
+            var target = targetItem.GetProviderId(provider);
+            if (!string.IsNullOrEmpty(source)
+                && (string.IsNullOrEmpty(target)
+                    || !target.Equals(source, StringComparison.Ordinal)))
+            {
+                targetItem.SetProviderId(provider, source);
+            }
+        }
+
+        private ItemUpdateType SetPeople(MusicAlbum item)
+        {
+            var updateType = ItemUpdateType.None;
+
+            if (item.AlbumArtists.Any() || item.Artists.Any())
+            {
+                var people = new List<PersonInfo>();
+
+                foreach (var albumArtist in item.AlbumArtists)
+                {
+                    PeopleHelper.AddPerson(people, new PersonInfo
+                    {
+                        Name = albumArtist,
+                        Type = "AlbumArtist"
+                    });
+                }
+
+                foreach (var artist in item.Artists)
+                {
+                    PeopleHelper.AddPerson(people, new PersonInfo
+                    {
+                        Name = artist,
+                        Type = "Artist"
+                    });
+                }
+
+                LibraryManager.UpdatePeople(item, people);
+                updateType |= ItemUpdateType.MetadataEdit;
+            }
+
+            return updateType;
+        }
+
         /// <inheritdoc />
         protected override void MergeData(
             MetadataResult<MusicAlbum> source,
@@ -123,6 +217,21 @@ namespace MediaBrowser.Providers.Music
             {
                 targetItem.Artists = sourceItem.Artists;
             }
+
+            if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist)))
+            {
+                SetProviderId(sourceItem, targetItem, MetadataProvider.MusicBrainzAlbumArtist);
+            }
+
+            if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzAlbum)))
+            {
+                SetProviderId(sourceItem, targetItem, MetadataProvider.MusicBrainzAlbum);
+            }
+
+            if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup)))
+            {
+                SetProviderId(sourceItem, targetItem, MetadataProvider.MusicBrainzReleaseGroup);
+            }
         }
     }
 }

+ 31 - 2
MediaBrowser.Providers/Music/AudioMetadataService.cs

@@ -1,5 +1,4 @@
-#pragma warning disable CS1591
-
+using System;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
@@ -11,8 +10,19 @@ using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Providers.Music
 {
+    /// <summary>
+    /// The audio metadata service.
+    /// </summary>
     public class AudioMetadataService : MetadataService<Audio, SongInfo>
     {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AudioMetadataService"/> class.
+        /// </summary>
+        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/>.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+        /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
         public AudioMetadataService(
             IServerConfigurationManager serverConfigurationManager,
             ILogger<AudioMetadataService> logger,
@@ -23,6 +33,21 @@ namespace MediaBrowser.Providers.Music
         {
         }
 
+        private void SetProviderId(Audio sourceItem, Audio targetItem, bool replaceData, MetadataProvider provider)
+        {
+            var target = targetItem.GetProviderId(provider);
+            if (replaceData || string.IsNullOrEmpty(target))
+            {
+                var source = sourceItem.GetProviderId(provider);
+                if (!string.IsNullOrEmpty(source)
+                    && (string.IsNullOrEmpty(target)
+                        || !target.Equals(source, StringComparison.Ordinal)))
+                {
+                    targetItem.SetProviderId(provider, source);
+                }
+            }
+        }
+
         /// <inheritdoc />
         protected override void MergeData(MetadataResult<Audio> source, MetadataResult<Audio> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
         {
@@ -40,6 +65,10 @@ namespace MediaBrowser.Providers.Music
             {
                 targetItem.Album = sourceItem.Album;
             }
+
+            SetProviderId(sourceItem, targetItem, replaceData, MetadataProvider.MusicBrainzAlbumArtist);
+            SetProviderId(sourceItem, targetItem, replaceData, MetadataProvider.MusicBrainzAlbum);
+            SetProviderId(sourceItem, targetItem, replaceData, MetadataProvider.MusicBrainzReleaseGroup);
         }
     }
 }