< Summary - Jellyfin

Information
Class: MediaBrowser.Providers.Plugins.MusicBrainz.MusicBrainzAlbumProvider
Assembly: MediaBrowser.Providers
File(s): /srv/git/jellyfin/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
Line coverage
1%
Covered lines: 2
Uncovered lines: 149
Coverable lines: 151
Total lines: 333
Line coverage: 1.3%
Branch coverage
0%
Covered branches: 0
Total branches: 136
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/16/2026 - 12:15:55 AM Line coverage: 0% (0/106) Branch coverage: 0% (0/80) Total lines: 2507/29/2026 - 12:15:53 AM Line coverage: 0% (0/143) Branch coverage: 0% (0/124) Total lines: 30710/29/2026 - 12:17:11 AM Line coverage: 1.3% (2/151) Branch coverage: 0% (0/136) Total lines: 333 5/16/2026 - 12:15:55 AM Line coverage: 0% (0/106) Branch coverage: 0% (0/80) Total lines: 2507/29/2026 - 12:15:53 AM Line coverage: 0% (0/143) Branch coverage: 0% (0/124) Total lines: 30710/29/2026 - 12:17:11 AM Line coverage: 1.3% (2/151) Branch coverage: 0% (0/136) Total lines: 333

Coverage delta

Coverage delta 2 -2

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Name()100%210%
get_Order()100%210%
GetSearchResults()0%210140%
GetReleaseSearchResult()0%2040%
GetReleaseGroupResultAsync()0%4260%
GetReleaseResult(...)0%342180%
GetMetadata()0%2550500%
Populate(...)0%1980440%
GetImageResponse(...)100%210%

File(s)

/srv/git/jellyfin/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Linq;
 4using System.Net.Http;
 5using System.Runtime.CompilerServices;
 6using System.Threading;
 7using System.Threading.Tasks;
 8using Jellyfin.Extensions;
 9using MediaBrowser.Controller.Entities.Audio;
 10using MediaBrowser.Controller.Providers;
 11using MediaBrowser.Model.Entities;
 12using MediaBrowser.Model.Providers;
 13using MediaBrowser.Providers.Music;
 14using MetaBrainz.MusicBrainz;
 15using MetaBrainz.MusicBrainz.Interfaces.Entities;
 16using MetaBrainz.MusicBrainz.Interfaces.Searches;
 17using Microsoft.Extensions.Logging;
 18
 19namespace MediaBrowser.Providers.Plugins.MusicBrainz;
 20
 21/// <summary>
 22/// Music album metadata provider for MusicBrainz.
 23/// </summary>
 24public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder
 25{
 26    private readonly ILogger<MusicBrainzAlbumProvider> _logger;
 27
 28    /// <summary>
 29    /// Initializes a new instance of the <see cref="MusicBrainzAlbumProvider"/> class.
 30    /// </summary>
 31    /// <param name="logger">The logger.</param>
 32    public MusicBrainzAlbumProvider(ILogger<MusicBrainzAlbumProvider> logger)
 33    {
 2734        _logger = logger;
 2735    }
 36
 37    /// <inheritdoc />
 038    public string Name => "MusicBrainz";
 39
 40    /// <inheritdoc />
 041    public int Order => 0;
 42
 43    /// <inheritdoc />
 44    public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancella
 45    {
 046        var query = MusicBrainz.Plugin.Instance!.MusicBrainzQuery;
 047        var releaseId = MusicBrainzQueryExtensions.ParseMusicBrainzId(searchInfo.GetReleaseId(), "release", _logger);
 048        var releaseGroupId = MusicBrainzQueryExtensions.ParseMusicBrainzId(searchInfo.GetReleaseGroupId(), "release grou
 49
 050        if (releaseId is not null)
 51        {
 052            var releaseResult = await query.LookupReleaseOrNullAsync(releaseId.Value, Include.Artists | Include.ReleaseG
 053            if (releaseResult is not null)
 54            {
 055                return GetReleaseResult(releaseResult).SingleItemAsEnumerable();
 56            }
 57        }
 58
 059        if (releaseGroupId is not null)
 60        {
 061            var releaseGroupResult = await query.LookupReleaseGroupOrNullAsync(releaseGroupId.Value, Include.Releases, _
 062            if (releaseGroupResult is not null)
 63            {
 64                // No need to pass the cancellation token to GetReleaseGroupResultAsync as we're already passing it to T
 065                return GetReleaseGroupResultAsync(releaseGroupResult.Releases, CancellationToken.None).ToBlockingEnumera
 66            }
 67        }
 68
 069        var artistMusicBrainzId = searchInfo.GetMusicBrainzArtistId();
 70
 071        if (!string.IsNullOrWhiteSpace(artistMusicBrainzId))
 72        {
 073            var releaseSearchResults = await query.FindReleasesWithRetryAsync($"\"{searchInfo.Name}\" AND arid:{artistMu
 074                .ConfigureAwait(false);
 75
 076            if (releaseSearchResults.Results.Count > 0)
 77            {
 078                return GetReleaseSearchResult(releaseSearchResults.Results);
 79            }
 80        }
 81        else
 82        {
 83            // I'm sure there is a better way but for now it resolves search for 12" Mixes
 084            var queryName = searchInfo.Name.Replace("\"", string.Empty, StringComparison.Ordinal);
 85
 086            var releaseSearchResults = await query.FindReleasesWithRetryAsync($"\"{queryName}\" AND artist:\"{searchInfo
 087                .ConfigureAwait(false);
 88
 089            if (releaseSearchResults.Results.Count > 0)
 90            {
 091                return GetReleaseSearchResult(releaseSearchResults.Results);
 92            }
 93        }
 94
 095        return Enumerable.Empty<RemoteSearchResult>();
 096    }
 97
 98    private IEnumerable<RemoteSearchResult> GetReleaseSearchResult(IEnumerable<ISearchResult<IRelease>>? releaseSearchRe
 99    {
 0100        if (releaseSearchResults is null)
 101        {
 0102            yield break;
 103        }
 104
 0105        foreach (var result in releaseSearchResults)
 106        {
 0107            yield return GetReleaseResult(result.Item);
 108        }
 0109    }
 110
 111    private async IAsyncEnumerable<RemoteSearchResult> GetReleaseGroupResultAsync(IEnumerable<IRelease>? releaseSearchRe
 112    {
 0113        if (releaseSearchResults is null)
 114        {
 0115            yield break;
 116        }
 117
 0118        var query = MusicBrainz.Plugin.Instance!.MusicBrainzQuery;
 0119        foreach (var result in releaseSearchResults)
 120        {
 121            // Fetch full release info, otherwise artists are missing
 0122            var fullResult = await query.LookupReleaseOrNullAsync(result.Id, Include.Artists | Include.ReleaseGroups, _l
 0123            if (fullResult is not null)
 124            {
 0125                yield return GetReleaseResult(fullResult);
 126            }
 127        }
 0128    }
 129
 130    private RemoteSearchResult GetReleaseResult(IRelease releaseSearchResult)
 131    {
 0132        var searchResult = new RemoteSearchResult
 0133        {
 0134            Name = releaseSearchResult.Title,
 0135            ProductionYear = releaseSearchResult.Date?.Year,
 0136            PremiereDate = releaseSearchResult.Date?.NearestDate,
 0137            SearchProviderName = Name
 0138        };
 139
 140        // Add artists and use first as album artist
 0141        var artists = releaseSearchResult.ArtistCredit;
 0142        if (artists is not null && artists.Count > 0)
 143        {
 0144            var artistResults = new RemoteSearchResult[artists.Count];
 0145            for (int i = 0; i < artists.Count; i++)
 146            {
 0147                var artist = artists[i];
 0148                var artistResult = new RemoteSearchResult
 0149                {
 0150                    Name = artist.Name
 0151                };
 152
 0153                if (artist.Artist?.Id is not null)
 154                {
 0155                    artistResult.SetProviderId(MetadataProvider.MusicBrainzArtist, artist.Artist!.Id.ToString());
 156                }
 157
 0158                artistResults[i] = artistResult;
 159            }
 160
 0161            searchResult.AlbumArtist = artistResults[0];
 0162            searchResult.Artists = artistResults;
 163        }
 164
 0165        searchResult.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseSearchResult.Id.ToString());
 166
 0167        if (releaseSearchResult.ReleaseGroup?.Id is not null)
 168        {
 0169            searchResult.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseSearchResult.ReleaseGroup.Id.ToS
 170        }
 171
 0172        return searchResult;
 173    }
 174
 175    /// <inheritdoc />
 176    public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo info, CancellationToken cancellationToken)
 177    {
 0178        var query = MusicBrainz.Plugin.Instance!.MusicBrainzQuery;
 0179        var releaseId = MusicBrainzQueryExtensions.ParseMusicBrainzId(info.GetReleaseId(), "release", _logger);
 0180        var releaseGroupId = MusicBrainzQueryExtensions.ParseMusicBrainzId(info.GetReleaseGroupId(), "release group", _l
 181
 0182        var result = new MetadataResult<MusicAlbum>
 0183        {
 0184            Item = new MusicAlbum()
 0185        };
 186
 187        // If there is a release group, but no release ID, try to match the release
 0188        if (releaseId is null && releaseGroupId is not null)
 189        {
 190            // TODO: Actually try to match the release. Simply taking the first result is stupid.
 0191            var releaseGroupLookup = await query.LookupReleaseGroupOrNullAsync(releaseGroupId.Value, Include.None, _logg
 0192            releaseId = releaseGroupLookup?.Releases?.Count > 0 ? releaseGroupLookup.Releases[0].Id : null;
 193        }
 194
 195        // If there is no release ID, lookup a release with the info we have
 0196        if (releaseId is null)
 197        {
 0198            var artistMusicBrainzId = info.GetMusicBrainzArtistId();
 0199            IRelease? releaseResult = null;
 200
 0201            if (!string.IsNullOrEmpty(artistMusicBrainzId))
 202            {
 0203                var releaseSearchResults = await query.FindReleasesWithRetryAsync($"\"{info.Name}\" AND arid:{artistMusi
 0204                    .ConfigureAwait(false);
 0205                releaseResult = releaseSearchResults.Results.Count > 0 ? releaseSearchResults.Results[0].Item : null;
 206            }
 0207            else if (!string.IsNullOrEmpty(info.GetAlbumArtist()))
 208            {
 0209                var releaseSearchResults = await query.FindReleasesWithRetryAsync($"\"{info.Name}\" AND artist:{info.Get
 0210                    .ConfigureAwait(false);
 0211                releaseResult = releaseSearchResults.Results.Count > 0 ? releaseSearchResults.Results[0].Item : null;
 212            }
 213
 0214            if (releaseResult is not null)
 215            {
 0216                releaseId = releaseResult.Id;
 217
 0218                if (releaseResult.ReleaseGroup?.Id is not null)
 219                {
 0220                    releaseGroupId = releaseResult.ReleaseGroup.Id;
 221                }
 222            }
 223        }
 224
 0225        if (releaseId is null && releaseGroupId is null)
 226        {
 0227            return result;
 228        }
 229
 230        // Fetch the full release (and its release group) so we can populate everything MusicBrainz returns.
 0231        IRelease? release = null;
 0232        if (releaseId is not null)
 233        {
 0234            release = await query.LookupReleaseOrNullAsync(
 0235                releaseId.Value,
 0236                Include.Artists | Include.ReleaseGroups | Include.Labels | Include.Genres | Include.Tags,
 0237                _logger,
 0238                cancellationToken).ConfigureAwait(false);
 239
 0240            if (releaseGroupId is null && release?.ReleaseGroup?.Id is not null)
 241            {
 0242                releaseGroupId = release.ReleaseGroup.Id;
 243            }
 244        }
 245
 0246        IReleaseGroup? releaseGroup = null;
 0247        if (releaseGroupId is not null)
 248        {
 0249            releaseGroup = await query.LookupReleaseGroupOrNullAsync(
 0250                releaseGroupId.Value,
 0251                Include.Artists | Include.Genres | Include.Tags,
 0252                _logger,
 0253                cancellationToken).ConfigureAwait(false);
 254        }
 255
 0256        if (release is null && releaseGroup is null)
 257        {
 0258            return result;
 259        }
 260
 0261        result.HasMetadata = true;
 262
 0263        if (releaseId is not null)
 264        {
 0265            result.Item.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseId.Value.ToString());
 266        }
 267
 0268        if (releaseGroupId is not null)
 269        {
 0270            result.Item.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseGroupId.Value.ToString());
 271        }
 272
 0273        Populate(result.Item, release, releaseGroup);
 274
 0275        return result;
 0276    }
 277
 278    private static void Populate(MusicAlbum item, IRelease? release, IReleaseGroup? releaseGroup)
 279    {
 280        // Prefer the release group (album-level) data, falling back to the specific release.
 281        // The release group's first release date is the original album date.
 0282        var date = releaseGroup?.FirstReleaseDate ?? release?.Date;
 0283        if (date is not null)
 284        {
 0285            item.PremiereDate = date.NearestDate;
 0286            item.ProductionYear = date.Year;
 287        }
 288
 0289        var artistCredit = release?.ArtistCredit ?? releaseGroup?.ArtistCredit;
 0290        if (artistCredit is not null && artistCredit.Count > 0)
 291        {
 0292            item.AlbumArtists = artistCredit
 0293                .Select(credit => credit.Artist?.Name ?? credit.Name)
 0294                .Where(name => !string.IsNullOrWhiteSpace(name))
 0295                .ToArray();
 296        }
 297
 0298        var genres = releaseGroup?.Genres ?? release?.Genres;
 0299        if (genres is not null && genres.Count > 0)
 300        {
 0301            item.Genres = genres
 0302                .OrderByDescending(genre => genre.VoteCount)
 0303                .Select(genre => genre.Name)
 0304                .Where(name => !string.IsNullOrWhiteSpace(name))
 0305                .ToArray();
 306        }
 307
 0308        var tags = releaseGroup?.Tags ?? release?.Tags;
 0309        if (tags is not null && tags.Count > 0)
 310        {
 0311            item.Tags = tags
 0312                .OrderByDescending(tag => tag.VoteCount)
 0313                .Select(tag => tag.Name)
 0314                .Where(name => !string.IsNullOrWhiteSpace(name))
 0315                .ToArray();
 316        }
 317
 0318        if (release?.LabelInfo is not null && release.LabelInfo.Count > 0)
 319        {
 0320            item.Studios = release.LabelInfo
 0321                .Where(labelInfo => !string.IsNullOrWhiteSpace(labelInfo.Label?.Name))
 0322                .Select(labelInfo => labelInfo.Label!.Name!)
 0323                .Distinct(StringComparer.OrdinalIgnoreCase)
 0324                .ToArray();
 325        }
 0326    }
 327
 328    /// <inheritdoc />
 329    public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
 330    {
 0331        throw new NotImplementedException();
 332    }
 333}