using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Providers; using MediaBrowser.Providers.Music; using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration; using MetaBrainz.MusicBrainz; using MetaBrainz.MusicBrainz.Interfaces.Entities; using MetaBrainz.MusicBrainz.Interfaces.Searches; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.MusicBrainz; /// /// Music album metadata provider for MusicBrainz. /// public class MusicBrainzAlbumProvider : IRemoteMetadataProvider, IHasOrder, IDisposable { private readonly ILogger _logger; private Query _musicBrainzQuery; /// /// Initializes a new instance of the class. /// /// The logger. public MusicBrainzAlbumProvider(ILogger logger) { _logger = logger; _musicBrainzQuery = new Query(); ReloadConfig(null, MusicBrainz.Plugin.Instance!.Configuration); MusicBrainz.Plugin.Instance!.ConfigurationChanged += ReloadConfig; } /// public string Name => "MusicBrainz"; /// public int Order => 0; private void ReloadConfig(object? sender, BasePluginConfiguration e) { var configuration = (PluginConfiguration)e; if (Uri.TryCreate(configuration.Server, UriKind.Absolute, out var server)) { Query.DefaultServer = server.DnsSafeHost; Query.DefaultPort = server.Port; Query.DefaultUrlScheme = server.Scheme; } else { // Fallback to official server _logger.LogWarning("Invalid MusicBrainz server specified, falling back to official server"); var defaultServer = new Uri(PluginConfiguration.DefaultServer); Query.DefaultServer = defaultServer.Host; Query.DefaultPort = defaultServer.Port; Query.DefaultUrlScheme = defaultServer.Scheme; } Query.DelayBetweenRequests = configuration.RateLimit; _musicBrainzQuery = new Query(); } /// public async Task> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken) { var releaseId = searchInfo.GetReleaseId(); var releaseGroupId = searchInfo.GetReleaseGroupId(); if (!string.IsNullOrEmpty(releaseId)) { var releaseResult = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.Artists | Include.ReleaseGroups, cancellationToken).ConfigureAwait(false); return GetReleaseResult(releaseResult).SingleItemAsEnumerable(); } if (!string.IsNullOrEmpty(releaseGroupId)) { var releaseGroupResult = await _musicBrainzQuery.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.Releases, null, cancellationToken).ConfigureAwait(false); return GetReleaseGroupResult(releaseGroupResult.Releases); } var artistMusicBrainzId = searchInfo.GetMusicBrainzArtistId(); if (!string.IsNullOrWhiteSpace(artistMusicBrainzId)) { var releaseSearchResults = await _musicBrainzQuery.FindReleasesAsync($"\"{searchInfo.Name}\" AND arid:{artistMusicBrainzId}", null, null, false, cancellationToken) .ConfigureAwait(false); if (releaseSearchResults.Results.Count > 0) { return GetReleaseSearchResult(releaseSearchResults.Results); } } else { // I'm sure there is a better way but for now it resolves search for 12" Mixes var queryName = searchInfo.Name.Replace("\"", string.Empty, StringComparison.Ordinal); var releaseSearchResults = await _musicBrainzQuery.FindReleasesAsync($"\"{queryName}\" AND artist:\"{searchInfo.GetAlbumArtist()}\"c", null, null, false, cancellationToken) .ConfigureAwait(false); if (releaseSearchResults.Results.Count > 0) { return GetReleaseSearchResult(releaseSearchResults.Results); } } return Enumerable.Empty(); } private IEnumerable GetReleaseSearchResult(IEnumerable>? releaseSearchResults) { if (releaseSearchResults is null) { yield break; } foreach (var result in releaseSearchResults) { yield return GetReleaseResult(result.Item); } } private IEnumerable GetReleaseGroupResult(IEnumerable? releaseSearchResults) { if (releaseSearchResults is null) { yield break; } foreach (var result in releaseSearchResults) { // Fetch full release info, otherwise artists are missing var fullResult = _musicBrainzQuery.LookupRelease(result.Id, Include.Artists | Include.ReleaseGroups); yield return GetReleaseResult(fullResult); } } private RemoteSearchResult GetReleaseResult(IRelease releaseSearchResult) { var searchResult = new RemoteSearchResult { Name = releaseSearchResult.Title, ProductionYear = releaseSearchResult.Date?.Year, PremiereDate = releaseSearchResult.Date?.NearestDate, SearchProviderName = Name }; // Add artists and use first as album artist var artists = releaseSearchResult.ArtistCredit; if (artists is not null && artists.Count > 0) { var artistResults = new List(); foreach (var artist in artists) { var artistResult = new RemoteSearchResult { Name = artist.Name }; if (artist.Artist?.Id is not null) { artistResult.SetProviderId(MetadataProvider.MusicBrainzArtist, artist.Artist!.Id.ToString()); } artistResults.Add(artistResult); } searchResult.AlbumArtist = artistResults[0]; searchResult.Artists = artistResults.ToArray(); } searchResult.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseSearchResult.Id.ToString()); if (releaseSearchResult.ReleaseGroup?.Id is not null) { searchResult.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseSearchResult.ReleaseGroup.Id.ToString()); } return searchResult; } /// public async Task> GetMetadata(AlbumInfo info, CancellationToken cancellationToken) { // TODO: This sets essentially nothing. As-is, it's mostly useless. Make it actually pull metadata and use it. var releaseId = info.GetReleaseId(); var releaseGroupId = info.GetReleaseGroupId(); var result = new MetadataResult { Item = new MusicAlbum() }; // If there is a release group, but no release ID, try to match the release if (string.IsNullOrWhiteSpace(releaseId) && !string.IsNullOrWhiteSpace(releaseGroupId)) { // TODO: Actually try to match the release. Simply taking the first result is stupid. var releaseGroup = await _musicBrainzQuery.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.None, null, cancellationToken).ConfigureAwait(false); var release = releaseGroup.Releases?.Count > 0 ? releaseGroup.Releases[0] : null; if (release is not null) { releaseId = release.Id.ToString(); result.HasMetadata = true; } } // If there is no release ID, lookup a release with the info we have if (string.IsNullOrWhiteSpace(releaseId)) { var artistMusicBrainzId = info.GetMusicBrainzArtistId(); IRelease? releaseResult = null; if (!string.IsNullOrEmpty(artistMusicBrainzId)) { var releaseSearchResults = await _musicBrainzQuery.FindReleasesAsync($"\"{info.Name}\" AND arid:{artistMusicBrainzId}", null, null, false, cancellationToken) .ConfigureAwait(false); releaseResult = releaseSearchResults.Results.Count > 0 ? releaseSearchResults.Results[0].Item : null; } else if (!string.IsNullOrEmpty(info.GetAlbumArtist())) { var releaseSearchResults = await _musicBrainzQuery.FindReleasesAsync($"\"{info.Name}\" AND artist:{info.GetAlbumArtist()}", null, null, false, cancellationToken) .ConfigureAwait(false); releaseResult = releaseSearchResults.Results.Count > 0 ? releaseSearchResults.Results[0].Item : null; } if (releaseResult is not null) { releaseId = releaseResult.Id.ToString(); if (releaseResult.ReleaseGroup?.Id is not null) { releaseGroupId = releaseResult.ReleaseGroup.Id.ToString(); } result.HasMetadata = true; result.Item.ProductionYear = releaseResult.Date?.Year; result.Item.Overview = releaseResult.Annotation; } } // If we have a release ID but not a release group ID, lookup the release group if (!string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId)) { var release = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.Releases, cancellationToken).ConfigureAwait(false); releaseGroupId = release.ReleaseGroup?.Id.ToString(); result.HasMetadata = true; } // If we have a release ID and a release group ID if (!string.IsNullOrWhiteSpace(releaseId) || !string.IsNullOrWhiteSpace(releaseGroupId)) { result.HasMetadata = true; } if (result.HasMetadata) { if (!string.IsNullOrEmpty(releaseId)) { result.Item.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseId); } if (!string.IsNullOrEmpty(releaseGroupId)) { result.Item.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseGroupId); } } return result; } /// public Task GetImageResponse(string url, CancellationToken cancellationToken) { throw new NotImplementedException(); } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Dispose all resources. /// /// Whether to dispose. protected virtual void Dispose(bool disposing) { if (disposing) { _musicBrainzQuery.Dispose(); } } }