diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index f09c4c876c..40f2775bd3 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -1038,6 +1038,11 @@ namespace MediaBrowser.Providers.Manager target.OriginalTitle = source.OriginalTitle; } + if (replaceData || string.IsNullOrEmpty(target.HomePageUrl)) + { + target.HomePageUrl = source.HomePageUrl; + } + if (replaceData || string.IsNullOrEmpty(target.OriginalLanguage)) { target.OriginalLanguage = source.OriginalLanguage; diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs index 88730f34d2..28cfc8f9a4 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs @@ -3,32 +3,24 @@ #pragma warning disable CS1591 using System.Collections.Generic; -using System.IO; using System.Net.Http; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Extensions.Json; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; namespace MediaBrowser.Providers.Plugins.AudioDb { public class AudioDbArtistImageProvider : IRemoteImageProvider, IHasOrder { - private readonly IServerConfigurationManager _config; private readonly IHttpClientFactory _httpClientFactory; - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - public AudioDbArtistImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory) + public AudioDbArtistImageProvider(IHttpClientFactory httpClientFactory) { - _config = config; _httpClientFactory = httpClientFactory; } @@ -54,22 +46,14 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) { - if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var id)) + item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var musicBrainzId); + item.TryGetProviderId(MetadataProvider.AudioDbArtist, out var audioDbId); + + var artist = await AudioDbArtistProvider.Current.GetArtist(musicBrainzId, audioDbId, cancellationToken).ConfigureAwait(false); + + if (artist is not null) { - await AudioDbArtistProvider.Current.EnsureArtistInfo(id, cancellationToken).ConfigureAwait(false); - - var path = AudioDbArtistProvider.GetArtistInfoPath(_config.ApplicationPaths, id); - - FileStream jsonStream = AsyncFile.OpenRead(path); - await using (jsonStream.ConfigureAwait(false)) - { - var obj = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); - - if (obj is not null && obj.artists is not null && obj.artists.Count > 0) - { - return GetImages(obj.artists[0]); - } - } + return GetImages(artist); } return []; diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs index d8cb6b4b24..c4f4833857 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs @@ -4,9 +4,11 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Net.Http; +using System.Net.Http.Json; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -52,45 +54,176 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public int Order => 1; /// - public Task> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken) - => Task.FromResult(Enumerable.Empty()); - - /// - public async Task> GetMetadata(ArtistInfo info, CancellationToken cancellationToken) + public async Task> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken) { - var result = new MetadataResult(); - var id = info.GetMusicBrainzArtistId(); - - if (!string.IsNullOrWhiteSpace(id)) + // Prefer a known TheAudioDB artist id. + var audioDbId = searchInfo.GetProviderId(MetadataProvider.AudioDbArtist); + if (!string.IsNullOrWhiteSpace(audioDbId)) { - await EnsureArtistInfo(id, cancellationToken).ConfigureAwait(false); + var artists = await FetchArtists(BaseUrl + "/artist.php?i=" + audioDbId, cancellationToken).ConfigureAwait(false); + return artists.Select(ToRemoteSearchResult); + } - var path = GetArtistInfoPath(_config.ApplicationPaths, id); + // Fall back to the MusicBrainz artist id, reusing the on-disk cache also used by GetMetadata. + var musicBrainzId = searchInfo.GetMusicBrainzArtistId(); + if (!string.IsNullOrWhiteSpace(musicBrainzId)) + { + await EnsureArtistInfo(musicBrainzId, cancellationToken).ConfigureAwait(false); + + var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId); FileStream jsonStream = AsyncFile.OpenRead(path); await using (jsonStream.ConfigureAwait(false)) { var obj = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); - if (obj is not null && obj.artists is not null && obj.artists.Count > 0) + if (obj is not null && obj.artists is not null) { - result.Item = new MusicArtist(); - result.HasMetadata = true; - ProcessResult(result.Item, obj.artists[0], info.MetadataLanguage); + return obj.artists.Select(ToRemoteSearchResult); } } + + return []; + } + + // Finally, search by name. + if (!string.IsNullOrWhiteSpace(searchInfo.Name)) + { + var artists = await FetchArtists(BaseUrl + "/search.php?s=" + Uri.EscapeDataString(searchInfo.Name), cancellationToken).ConfigureAwait(false); + return artists.Select(ToRemoteSearchResult); + } + + return []; + } + + private async Task> FetchArtists(string url, CancellationToken cancellationToken) + { + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + var obj = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false); + + return obj?.artists ?? []; + } + + private RemoteSearchResult ToRemoteSearchResult(Artist artist) + { + var result = new RemoteSearchResult + { + Name = artist.strArtist, + ImageUrl = artist.strArtistThumb, + SearchProviderName = Name, + Overview = (artist.strBiographyEN ?? string.Empty).StripHtml() + }; + + if (!string.IsNullOrEmpty(artist.idArtist)) + { + result.SetProviderId(MetadataProvider.AudioDbArtist, artist.idArtist); + } + + if (!string.IsNullOrEmpty(artist.strMusicBrainzID)) + { + result.SetProviderId(MetadataProvider.MusicBrainzArtist, artist.strMusicBrainzID); + } + + if (int.TryParse(artist.intFormedYear, NumberStyles.Integer, CultureInfo.InvariantCulture, out var formedYear)) + { + result.ProductionYear = formedYear; } return result; } + /// + public async Task> GetMetadata(ArtistInfo info, CancellationToken cancellationToken) + { + var result = new MetadataResult(); + + var artist = await GetArtist( + info.GetMusicBrainzArtistId(), + info.GetProviderId(MetadataProvider.AudioDbArtist), + cancellationToken).ConfigureAwait(false); + + if (artist is not null) + { + result.Item = new MusicArtist(); + result.HasMetadata = true; + ProcessResult(result.Item, artist, info.MetadataLanguage); + } + + return result; + } + + /// + /// Resolves the cached AudioDB artist, preferring the MusicBrainz id and falling back to the AudioDB id. + /// + /// The MusicBrainz artist id, if known. + /// The TheAudioDB artist id, if known. + /// The cancellation token. + /// The matching artist, or null if none could be resolved. + internal async Task GetArtist(string musicBrainzId, string audioDbId, CancellationToken cancellationToken) + { + string path; + if (!string.IsNullOrWhiteSpace(musicBrainzId)) + { + await EnsureArtistInfo(musicBrainzId, cancellationToken).ConfigureAwait(false); + path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId); + } + else if (!string.IsNullOrWhiteSpace(audioDbId)) + { + await EnsureArtistInfoByAudioDbId(audioDbId, cancellationToken).ConfigureAwait(false); + path = GetArtistInfoPath(_config.ApplicationPaths, audioDbId); + } + else + { + return null; + } + + FileStream jsonStream = AsyncFile.OpenRead(path); + await using (jsonStream.ConfigureAwait(false)) + { + var obj = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); + + if (obj is not null && obj.artists is not null && obj.artists.Count > 0) + { + return obj.artists[0]; + } + } + + return null; + } + private void ProcessResult(MusicArtist item, Artist result, string preferredLanguage) { - // item.HomePageUrl = result.strWebsite; - - if (!string.IsNullOrEmpty(result.strGenre)) + if (!string.IsNullOrWhiteSpace(result.strWebsite)) { - item.Genres = new[] { result.strGenre }; + item.HomePageUrl = result.strWebsite; + } + + var genres = new List(); + if (!string.IsNullOrWhiteSpace(result.strGenre)) + { + genres.Add(result.strGenre); + } + + if (!string.IsNullOrWhiteSpace(result.strSubGenre)) + { + genres.Add(result.strSubGenre); + } + + if (genres.Count > 0) + { + item.Genres = genres.ToArray(); + } + + if (int.TryParse(result.intFormedYear, NumberStyles.Integer, CultureInfo.InvariantCulture, out var formedYear)) + { + item.ProductionYear = formedYear; + } + + if (!string.IsNullOrWhiteSpace(result.strCountry)) + { + item.ProductionLocations = new[] { result.strCountry }; } item.SetProviderId(MetadataProvider.AudioDbArtist, result.idArtist); @@ -150,13 +283,32 @@ namespace MediaBrowser.Providers.Plugins.AudioDb internal async Task DownloadArtistInfo(string musicBrainzId, CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); - var url = BaseUrl + "/artist-mb.php?i=" + musicBrainzId; + await DownloadArtistInfo(url, GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId), cancellationToken).ConfigureAwait(false); + } + + internal async Task EnsureArtistInfoByAudioDbId(string audioDbId, CancellationToken cancellationToken) + { + var xmlPath = GetArtistInfoPath(_config.ApplicationPaths, audioDbId); + + var fileInfo = _fileSystem.GetFileSystemInfo(xmlPath); + + if (fileInfo.Exists + && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) + { + return; + } + + var url = BaseUrl + "/artist.php?i=" + audioDbId; + await DownloadArtistInfo(url, xmlPath, cancellationToken).ConfigureAwait(false); + } + + private async Task DownloadArtistInfo(string url, string path, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); - var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId); Directory.CreateDirectory(Path.GetDirectoryName(path)); var fileStreamOptions = AsyncFile.WriteOptions; diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs index 715bdd9da4..397c916a4f 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -155,7 +155,6 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider 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 query = MusicBrainz.Plugin.Instance!.MusicBrainzQuery; var releaseId = info.GetReleaseId(); var releaseGroupId = info.GetReleaseGroupId(); @@ -169,13 +168,8 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider 0 ? releaseGroup.Releases[0] : null; - if (release is not null) - { - releaseId = release.Id.ToString(); - result.HasMetadata = true; - } + var releaseGroupLookup = await query.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.None, null, cancellationToken).ConfigureAwait(false); + releaseId = releaseGroupLookup.Releases?.Count > 0 ? releaseGroupLookup.Releases[0].Id.ToString() : null; } // If there is no release ID, lookup a release with the info we have @@ -205,43 +199,106 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider 0) + { + item.AlbumArtists = artistCredit + .Select(credit => credit.Name) + .Where(name => !string.IsNullOrWhiteSpace(name)) + .ToArray(); + } + + var genres = releaseGroup?.Genres ?? release?.Genres; + if (genres is not null && genres.Count > 0) + { + item.Genres = genres + .OrderByDescending(genre => genre.VoteCount) + .Select(genre => genre.Name) + .Where(name => !string.IsNullOrWhiteSpace(name)) + .ToArray(); + } + + var tags = releaseGroup?.Tags ?? release?.Tags; + if (tags is not null && tags.Count > 0) + { + item.Tags = tags + .OrderByDescending(tag => tag.VoteCount) + .Select(tag => tag.Name) + .Where(name => !string.IsNullOrWhiteSpace(name)) + .ToArray(); + } + + if (release?.LabelInfo is not null && release.LabelInfo.Count > 0) + { + item.Studios = release.LabelInfo + .Where(labelInfo => !string.IsNullOrWhiteSpace(labelInfo.Label?.Name)) + .Select(labelInfo => labelInfo.Label!.Name!) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + } + /// public Task GetImageResponse(string url, CancellationToken cancellationToken) { diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs index 0fe4e6bb16..a9e950fb64 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs @@ -40,6 +40,11 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider 0) @@ -58,7 +63,7 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider(); + return []; } private IEnumerable GetResultsFromResponse(IEnumerable>? releaseSearchResults) @@ -96,28 +101,67 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider 0) + { + result.Item.Genres = artist.Genres + .OrderByDescending(genre => genre.VoteCount) + .Select(genre => genre.Name) + .Where(name => !string.IsNullOrWhiteSpace(name)) + .ToArray(); + } + + if (artist.Tags is not null && artist.Tags.Count > 0) + { + result.Item.Tags = artist.Tags + .OrderByDescending(tag => tag.VoteCount) + .Select(tag => tag.Name) + .Where(name => !string.IsNullOrWhiteSpace(name)) + .ToArray(); } return result;