diff options
| author | Claus Vium <cvium@users.noreply.github.com> | 2022-10-07 15:21:00 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-10-07 15:21:00 +0200 |
| commit | b137d0cc2bae86ffed23bdfcf6988c04a3b4d365 (patch) | |
| tree | d413e7029955e9b4dc603c6d772364ed4d3006df | |
| parent | f3c1ca07604c17046ffdad365f600552e028ccdc (diff) | |
| parent | 9b2e9640be029482a5d168a0ddb2feac0185d7a2 (diff) | |
Merge pull request #7514 from Shadowghost/music-extend
| -rw-r--r-- | Emby.Naming/Common/NamingOptions.cs | 23 | ||||
| -rw-r--r-- | Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs | 25 | ||||
| -rw-r--r-- | Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs | 28 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs | 2 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs | 6 | ||||
| -rw-r--r-- | MediaBrowser.Providers/MediaBrowser.Providers.csproj | 1 | ||||
| -rw-r--r-- | MediaBrowser.Providers/MediaInfo/AudioFileProber.cs | 215 | ||||
| -rw-r--r-- | MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs | 172 | ||||
| -rw-r--r-- | MediaBrowser.Providers/MediaInfo/ProbeProvider.cs (renamed from MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs) | 62 | ||||
| -rw-r--r-- | MediaBrowser.Providers/Music/AlbumMetadataService.cs | 133 | ||||
| -rw-r--r-- | MediaBrowser.Providers/Music/AudioMetadataService.cs | 33 |
11 files changed, 484 insertions, 216 deletions
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 29cc6558c..a72e0ffc7 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/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", @@ -745,6 +763,11 @@ namespace Emby.Naming.Common 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> public string[] SubtitleFileExtensions { get; set; } diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index da00b9cfa..a922e3685 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/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; } diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index 210ed0953..2538c2b5b 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/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(); } }); diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index bd397bdd1..6555de855 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/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. diff --git a/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs b/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs index 32a9cbef2..14428df5b 100644 --- a/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs +++ b/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); } } diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 9864db9ac..dd43ba9cb 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/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> diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs new file mode 100644 index 000000000..3699e8f49 --- /dev/null +++ b/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); + } + } + } +} diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs deleted file mode 100644 index f22965436..000000000 --- a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs +++ /dev/null @@ -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)); - } - } -} diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs index e58c0e281..659136607 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs +++ b/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 { diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs index 7743d3b27..ac40f0b3a 100644 --- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs +++ b/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); + } } } } diff --git a/MediaBrowser.Providers/Music/AudioMetadataService.cs b/MediaBrowser.Providers/Music/AudioMetadataService.cs index 4577f7745..a5b7cb895 100644 --- a/MediaBrowser.Providers/Music/AudioMetadataService.cs +++ b/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); } } } |
