Merge pull request #7514 from Shadowghost/music-extend

This commit is contained in:
Claus Vium 2022-10-07 15:21:00 +02:00 committed by GitHub
commit b137d0cc2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 484 additions and 216 deletions

View File

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

View File

@ -2,6 +2,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -18,7 +19,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Resolvers.Audio namespace Emby.Server.Implementations.Library.Resolvers.Audio
{ {
/// <summary> /// <summary>
/// Class MusicAlbumResolver. /// The music album resolver.
/// </summary> /// </summary>
public class MusicAlbumResolver : ItemResolver<MusicAlbum> public class MusicAlbumResolver : ItemResolver<MusicAlbum>
{ {
@ -82,7 +83,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// </summary> /// </summary>
/// <param name="path">The path to check.</param> /// <param name="path">The path to check.</param>
/// <param name="directoryService">The directory service.</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) public bool IsMusicAlbum(string path, IDirectoryService directoryService)
{ {
return ContainsMusic(directoryService.GetFileSystemEntries(path), true, 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> /// <returns><c>true</c> if [is music album] [the specified args]; otherwise, <c>false</c>.</returns>
private bool IsMusicAlbum(ItemResolveArgs args) 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.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)) if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService))
{ {
return true; return true;
@ -111,22 +121,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// <summary> /// <summary>
/// Determine if the supplied list contains what we should consider music. /// Determine if the supplied list contains what we should consider music.
/// </summary> /// </summary>
/// <returns><c>true</c> if the provided path list contains music; otherwise, <c>false</c>.</returns>
private bool ContainsMusic( private bool ContainsMusic(
ICollection<FileSystemMetadata> list, ICollection<FileSystemMetadata> list,
bool allowSubfolders, bool allowSubfolders,
IDirectoryService directoryService) 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)); var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && AudioFileParser.IsAudioFile(fileSystemInfo.FullName, _namingOptions));
if (foundAudioFile) if (foundAudioFile)
{ {
// at least one audio file exists // At least one audio file exists
return true; return true;
} }
if (!allowSubfolders) 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; return false;
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}
}
}
}

View File

@ -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));
}
}
}

View File

@ -1,7 +1,5 @@
#nullable disable #nullable disable
#pragma warning disable CS1591
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -27,7 +25,10 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.MediaInfo namespace MediaBrowser.Providers.MediaInfo
{ {
public class FFProbeProvider : ICustomMetadataProvider<Episode>, /// <summary>
/// The probe provider.
/// </summary>
public class ProbeProvider : ICustomMetadataProvider<Episode>,
ICustomMetadataProvider<MusicVideo>, ICustomMetadataProvider<MusicVideo>,
ICustomMetadataProvider<Movie>, ICustomMetadataProvider<Movie>,
ICustomMetadataProvider<Trailer>, ICustomMetadataProvider<Trailer>,
@ -39,14 +40,30 @@ namespace MediaBrowser.Providers.MediaInfo
IPreRefreshProvider, IPreRefreshProvider,
IHasItemChangeMonitor IHasItemChangeMonitor
{ {
private readonly ILogger<FFProbeProvider> _logger; private readonly ILogger<ProbeProvider> _logger;
private readonly AudioResolver _audioResolver; private readonly AudioResolver _audioResolver;
private readonly SubtitleResolver _subtitleResolver; private readonly SubtitleResolver _subtitleResolver;
private readonly FFProbeVideoInfo _videoProber; private readonly FFProbeVideoInfo _videoProber;
private readonly FFProbeAudioInfo _audioProber; private readonly AudioFileProber _audioProber;
private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None); 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, IMediaSourceManager mediaSourceManager,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
IItemRepository itemRepo, IItemRepository itemRepo,
@ -61,7 +78,8 @@ namespace MediaBrowser.Providers.MediaInfo
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
NamingOptions namingOptions) 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); _audioResolver = new AudioResolver(loggerFactory.CreateLogger<AudioResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
_subtitleResolver = new SubtitleResolver(loggerFactory.CreateLogger<SubtitleResolver>(), localization, mediaEncoder, fileSystem, namingOptions); _subtitleResolver = new SubtitleResolver(loggerFactory.CreateLogger<SubtitleResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
_videoProber = new FFProbeVideoInfo( _videoProber = new FFProbeVideoInfo(
@ -78,14 +96,15 @@ namespace MediaBrowser.Providers.MediaInfo
libraryManager, libraryManager,
_audioResolver, _audioResolver,
_subtitleResolver); _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; public int Order => 100;
/// <inheritdoc />
public bool HasChanged(BaseItem item, IDirectoryService directoryService) public bool HasChanged(BaseItem item, IDirectoryService directoryService)
{ {
var video = item as Video; var video = item as Video;
@ -127,41 +146,56 @@ namespace MediaBrowser.Providers.MediaInfo
return false; return false;
} }
/// <inheritdoc />
public Task<ItemUpdateType> FetchAsync(Episode item, MetadataRefreshOptions options, CancellationToken cancellationToken) public Task<ItemUpdateType> FetchAsync(Episode item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{ {
return FetchVideoInfo(item, options, cancellationToken); return FetchVideoInfo(item, options, cancellationToken);
} }
/// <inheritdoc />
public Task<ItemUpdateType> FetchAsync(MusicVideo item, MetadataRefreshOptions options, CancellationToken cancellationToken) public Task<ItemUpdateType> FetchAsync(MusicVideo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{ {
return FetchVideoInfo(item, options, cancellationToken); return FetchVideoInfo(item, options, cancellationToken);
} }
/// <inheritdoc />
public Task<ItemUpdateType> FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken) public Task<ItemUpdateType> FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{ {
return FetchVideoInfo(item, options, cancellationToken); return FetchVideoInfo(item, options, cancellationToken);
} }
/// <inheritdoc />
public Task<ItemUpdateType> FetchAsync(Trailer item, MetadataRefreshOptions options, CancellationToken cancellationToken) public Task<ItemUpdateType> FetchAsync(Trailer item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{ {
return FetchVideoInfo(item, options, cancellationToken); return FetchVideoInfo(item, options, cancellationToken);
} }
/// <inheritdoc />
public Task<ItemUpdateType> FetchAsync(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken) public Task<ItemUpdateType> FetchAsync(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{ {
return FetchVideoInfo(item, options, cancellationToken); return FetchVideoInfo(item, options, cancellationToken);
} }
/// <inheritdoc />
public Task<ItemUpdateType> FetchAsync(Audio item, MetadataRefreshOptions options, CancellationToken cancellationToken) public Task<ItemUpdateType> FetchAsync(Audio item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{ {
return FetchAudioInfo(item, options, cancellationToken); return FetchAudioInfo(item, options, cancellationToken);
} }
/// <inheritdoc />
public Task<ItemUpdateType> FetchAsync(AudioBook item, MetadataRefreshOptions options, CancellationToken cancellationToken) public Task<ItemUpdateType> FetchAsync(AudioBook item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{ {
return FetchAudioInfo(item, options, 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) public Task<ItemUpdateType> FetchVideoInfo<T>(T item, MetadataRefreshOptions options, CancellationToken cancellationToken)
where T : Video where T : Video
{ {
@ -208,6 +242,14 @@ namespace MediaBrowser.Providers.MediaInfo
.FirstOrDefault(i => !string.IsNullOrWhiteSpace(i) && !i.StartsWith('#')); .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) public Task<ItemUpdateType> FetchAudioInfo<T>(T item, MetadataRefreshOptions options, CancellationToken cancellationToken)
where T : Audio where T : Audio
{ {

View File

@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -15,8 +13,19 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Music namespace MediaBrowser.Providers.Music
{ {
/// <summary>
/// The album metadata service.
/// </summary>
public class AlbumMetadataService : MetadataService<MusicAlbum, AlbumInfo> 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( public AlbumMetadataService(
IServerConfigurationManager serverConfigurationManager, IServerConfigurationManager serverConfigurationManager,
ILogger<AlbumMetadataService> logger, ILogger<AlbumMetadataService> logger,
@ -61,40 +70,46 @@ namespace MediaBrowser.Providers.Music
var songs = children.Cast<Audio>().ToArray(); var songs = children.Cast<Audio>().ToArray();
updateType |= SetAlbumArtistFromSongs(item, songs);
updateType |= SetArtistsFromSongs(item, songs); updateType |= SetArtistsFromSongs(item, songs);
updateType |= SetAlbumArtistFromSongs(item, songs);
updateType |= SetAlbumFromSongs(item, songs);
updateType |= SetPeople(item);
} }
return updateType; return updateType;
} }
private ItemUpdateType SetAlbumArtistFromSongs(MusicAlbum item, IEnumerable<Audio> songs) private ItemUpdateType SetAlbumArtistFromSongs(MusicAlbum item, IReadOnlyList<Audio> songs)
{ {
var updateType = ItemUpdateType.None; var updateType = ItemUpdateType.None;
var artists = songs var albumArtists = songs
.SelectMany(i => i.AlbumArtists) .SelectMany(i => i.AlbumArtists)
.Distinct(StringComparer.OrdinalIgnoreCase) .GroupBy(i => i)
.OrderBy(i => i) .OrderByDescending(g => g.Count())
.Select(g => g.Key)
.ToArray(); .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; updateType |= ItemUpdateType.MetadataEdit;
} }
return updateType; return updateType;
} }
private ItemUpdateType SetArtistsFromSongs(MusicAlbum item, IEnumerable<Audio> songs) private ItemUpdateType SetArtistsFromSongs(MusicAlbum item, IReadOnlyList<Audio> songs)
{ {
var updateType = ItemUpdateType.None; var updateType = ItemUpdateType.None;
var artists = songs var artists = songs
.SelectMany(i => i.Artists) .SelectMany(i => i.Artists)
.Distinct(StringComparer.OrdinalIgnoreCase) .GroupBy(i => i)
.OrderBy(i => i) .OrderByDescending(g => g.Count())
.Select(g => g.Key)
.ToArray(); .ToArray();
if (!item.Artists.SequenceEqual(artists, StringComparer.OrdinalIgnoreCase)) if (!item.Artists.SequenceEqual(artists, StringComparer.OrdinalIgnoreCase))
@ -106,6 +121,85 @@ namespace MediaBrowser.Providers.Music
return updateType; 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 /> /// <inheritdoc />
protected override void MergeData( protected override void MergeData(
MetadataResult<MusicAlbum> source, MetadataResult<MusicAlbum> source,
@ -123,6 +217,21 @@ namespace MediaBrowser.Providers.Music
{ {
targetItem.Artists = sourceItem.Artists; 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);
}
} }
} }
} }

View File

@ -1,5 +1,4 @@
#pragma warning disable CS1591 using System;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -11,8 +10,19 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Music namespace MediaBrowser.Providers.Music
{ {
/// <summary>
/// The audio metadata service.
/// </summary>
public class AudioMetadataService : MetadataService<Audio, SongInfo> 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( public AudioMetadataService(
IServerConfigurationManager serverConfigurationManager, IServerConfigurationManager serverConfigurationManager,
ILogger<AudioMetadataService> logger, 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 /> /// <inheritdoc />
protected override void MergeData(MetadataResult<Audio> source, MetadataResult<Audio> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings) 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; targetItem.Album = sourceItem.Album;
} }
SetProviderId(sourceItem, targetItem, replaceData, MetadataProvider.MusicBrainzAlbumArtist);
SetProviderId(sourceItem, targetItem, replaceData, MetadataProvider.MusicBrainzAlbum);
SetProviderId(sourceItem, targetItem, replaceData, MetadataProvider.MusicBrainzReleaseGroup);
} }
} }
} }