< 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
0%
Covered lines: 0
Uncovered lines: 143
Coverable lines: 143
Total lines: 307
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 124
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/6/2026 - 12:15:23 AM Line coverage: 14.9% (19/127) Branch coverage: 3.5% (3/84) Total lines: 3075/7/2026 - 12:15:44 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: 307 5/6/2026 - 12:15:23 AM Line coverage: 14.9% (19/127) Branch coverage: 3.5% (3/84) Total lines: 3075/7/2026 - 12:15:44 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: 307

Coverage delta

Coverage delta 15 -15

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Name()100%210%
get_Order()100%210%
GetSearchResults()0%110100%
GetReleaseSearchResult()0%2040%
GetReleaseGroupResultAsync()0%2040%
GetReleaseResult(...)0%342180%
GetMetadata()0%1980440%
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;
 17
 18namespace MediaBrowser.Providers.Plugins.MusicBrainz;
 19
 20/// <summary>
 21/// Music album metadata provider for MusicBrainz.
 22/// </summary>
 23public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder
 24{
 25    /// <inheritdoc />
 026    public string Name => "MusicBrainz";
 27
 28    /// <inheritdoc />
 029    public int Order => 0;
 30
 31    /// <inheritdoc />
 32    public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancella
 33    {
 034        var query = MusicBrainz.Plugin.Instance!.MusicBrainzQuery;
 035        var releaseId = searchInfo.GetReleaseId();
 036        var releaseGroupId = searchInfo.GetReleaseGroupId();
 37
 038        if (!string.IsNullOrEmpty(releaseId))
 39        {
 040            var releaseResult = await query.LookupReleaseAsync(new Guid(releaseId), Include.Artists | Include.ReleaseGro
 041            return GetReleaseResult(releaseResult).SingleItemAsEnumerable();
 42        }
 43
 044        if (!string.IsNullOrEmpty(releaseGroupId))
 45        {
 046            var releaseGroupResult = await query.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.Releases, nul
 47
 48            // No need to pass the cancellation token to GetReleaseGroupResultAsync as we're already passing it to ToBlo
 049            return GetReleaseGroupResultAsync(releaseGroupResult.Releases, CancellationToken.None).ToBlockingEnumerable(
 50        }
 51
 052        var artistMusicBrainzId = searchInfo.GetMusicBrainzArtistId();
 53
 054        if (!string.IsNullOrWhiteSpace(artistMusicBrainzId))
 55        {
 056            var releaseSearchResults = await query.FindReleasesAsync($"\"{searchInfo.Name}\" AND arid:{artistMusicBrainz
 057                .ConfigureAwait(false);
 58
 059            if (releaseSearchResults.Results.Count > 0)
 60            {
 061                return GetReleaseSearchResult(releaseSearchResults.Results);
 62            }
 63        }
 64        else
 65        {
 66            // I'm sure there is a better way but for now it resolves search for 12" Mixes
 067            var queryName = searchInfo.Name.Replace("\"", string.Empty, StringComparison.Ordinal);
 68
 069            var releaseSearchResults = await query.FindReleasesAsync($"\"{queryName}\" AND artist:\"{searchInfo.GetAlbum
 070                .ConfigureAwait(false);
 71
 072            if (releaseSearchResults.Results.Count > 0)
 73            {
 074                return GetReleaseSearchResult(releaseSearchResults.Results);
 75            }
 76        }
 77
 078        return Enumerable.Empty<RemoteSearchResult>();
 079    }
 80
 81    private IEnumerable<RemoteSearchResult> GetReleaseSearchResult(IEnumerable<ISearchResult<IRelease>>? releaseSearchRe
 82    {
 083        if (releaseSearchResults is null)
 84        {
 085            yield break;
 86        }
 87
 088        foreach (var result in releaseSearchResults)
 89        {
 090            yield return GetReleaseResult(result.Item);
 91        }
 092    }
 93
 94    private async IAsyncEnumerable<RemoteSearchResult> GetReleaseGroupResultAsync(IEnumerable<IRelease>? releaseSearchRe
 95    {
 096        if (releaseSearchResults is null)
 97        {
 098            yield break;
 99        }
 100
 0101        var query = MusicBrainz.Plugin.Instance!.MusicBrainzQuery;
 0102        foreach (var result in releaseSearchResults)
 103        {
 104            // Fetch full release info, otherwise artists are missing
 0105            var fullResult = await query.LookupReleaseAsync(result.Id, Include.Artists | Include.ReleaseGroups, cancella
 0106            yield return GetReleaseResult(fullResult);
 107        }
 0108    }
 109
 110    private RemoteSearchResult GetReleaseResult(IRelease releaseSearchResult)
 111    {
 0112        var searchResult = new RemoteSearchResult
 0113        {
 0114            Name = releaseSearchResult.Title,
 0115            ProductionYear = releaseSearchResult.Date?.Year,
 0116            PremiereDate = releaseSearchResult.Date?.NearestDate,
 0117            SearchProviderName = Name
 0118        };
 119
 120        // Add artists and use first as album artist
 0121        var artists = releaseSearchResult.ArtistCredit;
 0122        if (artists is not null && artists.Count > 0)
 123        {
 0124            var artistResults = new RemoteSearchResult[artists.Count];
 0125            for (int i = 0; i < artists.Count; i++)
 126            {
 0127                var artist = artists[i];
 0128                var artistResult = new RemoteSearchResult
 0129                {
 0130                    Name = artist.Name
 0131                };
 132
 0133                if (artist.Artist?.Id is not null)
 134                {
 0135                    artistResult.SetProviderId(MetadataProvider.MusicBrainzArtist, artist.Artist!.Id.ToString());
 136                }
 137
 0138                artistResults[i] = artistResult;
 139            }
 140
 0141            searchResult.AlbumArtist = artistResults[0];
 0142            searchResult.Artists = artistResults;
 143        }
 144
 0145        searchResult.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseSearchResult.Id.ToString());
 146
 0147        if (releaseSearchResult.ReleaseGroup?.Id is not null)
 148        {
 0149            searchResult.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseSearchResult.ReleaseGroup.Id.ToS
 150        }
 151
 0152        return searchResult;
 153    }
 154
 155    /// <inheritdoc />
 156    public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo info, CancellationToken cancellationToken)
 157    {
 0158        var query = MusicBrainz.Plugin.Instance!.MusicBrainzQuery;
 0159        var releaseId = info.GetReleaseId();
 0160        var releaseGroupId = info.GetReleaseGroupId();
 161
 0162        var result = new MetadataResult<MusicAlbum>
 0163        {
 0164            Item = new MusicAlbum()
 0165        };
 166
 167        // If there is a release group, but no release ID, try to match the release
 0168        if (string.IsNullOrWhiteSpace(releaseId) && !string.IsNullOrWhiteSpace(releaseGroupId))
 169        {
 170            // TODO: Actually try to match the release. Simply taking the first result is stupid.
 0171            var releaseGroupLookup = await query.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.None, null, c
 0172            releaseId = releaseGroupLookup.Releases?.Count > 0 ? releaseGroupLookup.Releases[0].Id.ToString() : null;
 173        }
 174
 175        // If there is no release ID, lookup a release with the info we have
 0176        if (string.IsNullOrWhiteSpace(releaseId))
 177        {
 0178            var artistMusicBrainzId = info.GetMusicBrainzArtistId();
 0179            IRelease? releaseResult = null;
 180
 0181            if (!string.IsNullOrEmpty(artistMusicBrainzId))
 182            {
 0183                var releaseSearchResults = await query.FindReleasesAsync($"\"{info.Name}\" AND arid:{artistMusicBrainzId
 0184                    .ConfigureAwait(false);
 0185                releaseResult = releaseSearchResults.Results.Count > 0 ? releaseSearchResults.Results[0].Item : null;
 186            }
 0187            else if (!string.IsNullOrEmpty(info.GetAlbumArtist()))
 188            {
 0189                var releaseSearchResults = await query.FindReleasesAsync($"\"{info.Name}\" AND artist:{info.GetAlbumArti
 0190                    .ConfigureAwait(false);
 0191                releaseResult = releaseSearchResults.Results.Count > 0 ? releaseSearchResults.Results[0].Item : null;
 192            }
 193
 0194            if (releaseResult is not null)
 195            {
 0196                releaseId = releaseResult.Id.ToString();
 197
 0198                if (releaseResult.ReleaseGroup?.Id is not null)
 199                {
 0200                    releaseGroupId = releaseResult.ReleaseGroup.Id.ToString();
 201                }
 202            }
 203        }
 204
 0205        if (string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId))
 206        {
 0207            return result;
 208        }
 209
 210        // Fetch the full release (and its release group) so we can populate everything MusicBrainz returns.
 0211        IRelease? release = null;
 0212        if (!string.IsNullOrWhiteSpace(releaseId))
 213        {
 0214            release = await query.LookupReleaseAsync(
 0215                new Guid(releaseId),
 0216                Include.Artists | Include.ReleaseGroups | Include.Labels | Include.Genres | Include.Tags,
 0217                cancellationToken).ConfigureAwait(false);
 218
 0219            if (string.IsNullOrWhiteSpace(releaseGroupId) && release?.ReleaseGroup?.Id is not null)
 220            {
 0221                releaseGroupId = release.ReleaseGroup.Id.ToString();
 222            }
 223        }
 224
 0225        IReleaseGroup? releaseGroup = null;
 0226        if (!string.IsNullOrWhiteSpace(releaseGroupId))
 227        {
 0228            releaseGroup = await query.LookupReleaseGroupAsync(
 0229                new Guid(releaseGroupId),
 0230                Include.Artists | Include.Genres | Include.Tags,
 0231                null,
 0232                cancellationToken).ConfigureAwait(false);
 233        }
 234
 0235        result.HasMetadata = true;
 236
 0237        if (!string.IsNullOrEmpty(releaseId))
 238        {
 0239            result.Item.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseId);
 240        }
 241
 0242        if (!string.IsNullOrEmpty(releaseGroupId))
 243        {
 0244            result.Item.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseGroupId);
 245        }
 246
 0247        Populate(result.Item, release, releaseGroup);
 248
 0249        return result;
 0250    }
 251
 252    private static void Populate(MusicAlbum item, IRelease? release, IReleaseGroup? releaseGroup)
 253    {
 254        // Prefer the release group (album-level) data, falling back to the specific release.
 255        // The release group's first release date is the original album date.
 0256        var date = releaseGroup?.FirstReleaseDate ?? release?.Date;
 0257        if (date is not null)
 258        {
 0259            item.PremiereDate = date.NearestDate;
 0260            item.ProductionYear = date.Year;
 261        }
 262
 0263        var artistCredit = release?.ArtistCredit ?? releaseGroup?.ArtistCredit;
 0264        if (artistCredit is not null && artistCredit.Count > 0)
 265        {
 0266            item.AlbumArtists = artistCredit
 0267                .Select(credit => credit.Name)
 0268                .Where(name => !string.IsNullOrWhiteSpace(name))
 0269                .ToArray();
 270        }
 271
 0272        var genres = releaseGroup?.Genres ?? release?.Genres;
 0273        if (genres is not null && genres.Count > 0)
 274        {
 0275            item.Genres = genres
 0276                .OrderByDescending(genre => genre.VoteCount)
 0277                .Select(genre => genre.Name)
 0278                .Where(name => !string.IsNullOrWhiteSpace(name))
 0279                .ToArray();
 280        }
 281
 0282        var tags = releaseGroup?.Tags ?? release?.Tags;
 0283        if (tags is not null && tags.Count > 0)
 284        {
 0285            item.Tags = tags
 0286                .OrderByDescending(tag => tag.VoteCount)
 0287                .Select(tag => tag.Name)
 0288                .Where(name => !string.IsNullOrWhiteSpace(name))
 0289                .ToArray();
 290        }
 291
 0292        if (release?.LabelInfo is not null && release.LabelInfo.Count > 0)
 293        {
 0294            item.Studios = release.LabelInfo
 0295                .Where(labelInfo => !string.IsNullOrWhiteSpace(labelInfo.Label?.Name))
 0296                .Select(labelInfo => labelInfo.Label!.Name!)
 0297                .Distinct(StringComparer.OrdinalIgnoreCase)
 0298                .ToArray();
 299        }
 0300    }
 301
 302    /// <inheritdoc />
 303    public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
 304    {
 0305        throw new NotImplementedException();
 306    }
 307}