< 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
36%
Covered lines: 19
Uncovered lines: 33
Coverable lines: 52
Total lines: 307
Line coverage: 36.5%
Branch coverage
13%
Covered branches: 3
Total branches: 22
Branch coverage: 13.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 10/17/2025 - 12:10:14 AM Line coverage: 36.5% (19/52) Branch coverage: 13.6% (3/22) Total lines: 3041/4/2026 - 12:12:33 AM Line coverage: 36.5% (19/52) Branch coverage: 13.6% (3/22) Total lines: 307 10/17/2025 - 12:10:14 AM Line coverage: 36.5% (19/52) Branch coverage: 13.6% (3/22) Total lines: 3041/4/2026 - 12:12:33 AM Line coverage: 36.5% (19/52) Branch coverage: 13.6% (3/22) Total lines: 307

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Name()100%210%
get_Order()100%210%
ReloadConfig(...)50%2261.53%
GetReleaseResult(...)0%342180%
GetImageResponse(...)100%210%
Dispose()100%11100%
Dispose(...)100%22100%

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.Plugins;
 13using MediaBrowser.Model.Providers;
 14using MediaBrowser.Providers.Music;
 15using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
 16using MetaBrainz.MusicBrainz;
 17using MetaBrainz.MusicBrainz.Interfaces.Entities;
 18using MetaBrainz.MusicBrainz.Interfaces.Searches;
 19using Microsoft.Extensions.Logging;
 20
 21namespace MediaBrowser.Providers.Plugins.MusicBrainz;
 22
 23/// <summary>
 24/// Music album metadata provider for MusicBrainz.
 25/// </summary>
 26public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder, IDisposable
 27{
 28    private readonly ILogger<MusicBrainzAlbumProvider> _logger;
 29    private Query _musicBrainzQuery;
 30
 31    /// <summary>
 32    /// Initializes a new instance of the <see cref="MusicBrainzAlbumProvider"/> class.
 33    /// </summary>
 34    /// <param name="logger">The logger.</param>
 35    public MusicBrainzAlbumProvider(ILogger<MusicBrainzAlbumProvider> logger)
 36    {
 2137        _logger = logger;
 2138        _musicBrainzQuery = new Query();
 2139        ReloadConfig(null, MusicBrainz.Plugin.Instance!.Configuration);
 2140        MusicBrainz.Plugin.Instance!.ConfigurationChanged += ReloadConfig;
 2141    }
 42
 43    /// <inheritdoc />
 044    public string Name => "MusicBrainz";
 45
 46    /// <inheritdoc />
 047    public int Order => 0;
 48
 49    private void ReloadConfig(object? sender, BasePluginConfiguration e)
 50    {
 2151        var configuration = (PluginConfiguration)e;
 2152        if (Uri.TryCreate(configuration.Server, UriKind.Absolute, out var server))
 53        {
 2154            Query.DefaultServer = server.DnsSafeHost;
 2155            Query.DefaultPort = server.Port;
 2156            Query.DefaultUrlScheme = server.Scheme;
 57        }
 58        else
 59        {
 60            // Fallback to official server
 061            _logger.LogWarning("Invalid MusicBrainz server specified, falling back to official server");
 062            var defaultServer = new Uri(PluginConfiguration.DefaultServer);
 063            Query.DefaultServer = defaultServer.Host;
 064            Query.DefaultPort = defaultServer.Port;
 065            Query.DefaultUrlScheme = defaultServer.Scheme;
 66        }
 67
 2168        Query.DelayBetweenRequests = configuration.RateLimit;
 2169        _musicBrainzQuery = new Query();
 2170    }
 71
 72    /// <inheritdoc />
 73    public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancella
 74    {
 75        var releaseId = searchInfo.GetReleaseId();
 76        var releaseGroupId = searchInfo.GetReleaseGroupId();
 77
 78        if (!string.IsNullOrEmpty(releaseId))
 79        {
 80            var releaseResult = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.Artists | Includ
 81            return GetReleaseResult(releaseResult).SingleItemAsEnumerable();
 82        }
 83
 84        if (!string.IsNullOrEmpty(releaseGroupId))
 85        {
 86            var releaseGroupResult = await _musicBrainzQuery.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.R
 87
 88            // No need to pass the cancellation token to GetReleaseGroupResultAsync as we're already passing it to ToBlo
 89            return GetReleaseGroupResultAsync(releaseGroupResult.Releases, CancellationToken.None).ToBlockingEnumerable(
 90        }
 91
 92        var artistMusicBrainzId = searchInfo.GetMusicBrainzArtistId();
 93
 94        if (!string.IsNullOrWhiteSpace(artistMusicBrainzId))
 95        {
 96            var releaseSearchResults = await _musicBrainzQuery.FindReleasesAsync($"\"{searchInfo.Name}\" AND arid:{artis
 97                .ConfigureAwait(false);
 98
 99            if (releaseSearchResults.Results.Count > 0)
 100            {
 101                return GetReleaseSearchResult(releaseSearchResults.Results);
 102            }
 103        }
 104        else
 105        {
 106            // I'm sure there is a better way but for now it resolves search for 12" Mixes
 107            var queryName = searchInfo.Name.Replace("\"", string.Empty, StringComparison.Ordinal);
 108
 109            var releaseSearchResults = await _musicBrainzQuery.FindReleasesAsync($"\"{queryName}\" AND artist:\"{searchI
 110                .ConfigureAwait(false);
 111
 112            if (releaseSearchResults.Results.Count > 0)
 113            {
 114                return GetReleaseSearchResult(releaseSearchResults.Results);
 115            }
 116        }
 117
 118        return Enumerable.Empty<RemoteSearchResult>();
 119    }
 120
 121    private IEnumerable<RemoteSearchResult> GetReleaseSearchResult(IEnumerable<ISearchResult<IRelease>>? releaseSearchRe
 122    {
 123        if (releaseSearchResults is null)
 124        {
 125            yield break;
 126        }
 127
 128        foreach (var result in releaseSearchResults)
 129        {
 130            yield return GetReleaseResult(result.Item);
 131        }
 132    }
 133
 134    private async IAsyncEnumerable<RemoteSearchResult> GetReleaseGroupResultAsync(IEnumerable<IRelease>? releaseSearchRe
 135    {
 136        if (releaseSearchResults is null)
 137        {
 138            yield break;
 139        }
 140
 141        foreach (var result in releaseSearchResults)
 142        {
 143            // Fetch full release info, otherwise artists are missing
 144            var fullResult = await _musicBrainzQuery.LookupReleaseAsync(result.Id, Include.Artists | Include.ReleaseGrou
 145            yield return GetReleaseResult(fullResult);
 146        }
 147    }
 148
 149    private RemoteSearchResult GetReleaseResult(IRelease releaseSearchResult)
 150    {
 0151        var searchResult = new RemoteSearchResult
 0152        {
 0153            Name = releaseSearchResult.Title,
 0154            ProductionYear = releaseSearchResult.Date?.Year,
 0155            PremiereDate = releaseSearchResult.Date?.NearestDate,
 0156            SearchProviderName = Name
 0157        };
 158
 159        // Add artists and use first as album artist
 0160        var artists = releaseSearchResult.ArtistCredit;
 0161        if (artists is not null && artists.Count > 0)
 162        {
 0163            var artistResults = new RemoteSearchResult[artists.Count];
 0164            for (int i = 0; i < artists.Count; i++)
 165            {
 0166                var artist = artists[i];
 0167                var artistResult = new RemoteSearchResult
 0168                {
 0169                    Name = artist.Name
 0170                };
 171
 0172                if (artist.Artist?.Id is not null)
 173                {
 0174                    artistResult.SetProviderId(MetadataProvider.MusicBrainzArtist, artist.Artist!.Id.ToString());
 175                }
 176
 0177                artistResults[i] = artistResult;
 178            }
 179
 0180            searchResult.AlbumArtist = artistResults[0];
 0181            searchResult.Artists = artistResults;
 182        }
 183
 0184        searchResult.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseSearchResult.Id.ToString());
 185
 0186        if (releaseSearchResult.ReleaseGroup?.Id is not null)
 187        {
 0188            searchResult.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseSearchResult.ReleaseGroup.Id.ToS
 189        }
 190
 0191        return searchResult;
 192    }
 193
 194    /// <inheritdoc />
 195    public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo info, CancellationToken cancellationToken)
 196    {
 197        // TODO: This sets essentially nothing. As-is, it's mostly useless. Make it actually pull metadata and use it.
 198        var releaseId = info.GetReleaseId();
 199        var releaseGroupId = info.GetReleaseGroupId();
 200
 201        var result = new MetadataResult<MusicAlbum>
 202        {
 203            Item = new MusicAlbum()
 204        };
 205
 206        // If there is a release group, but no release ID, try to match the release
 207        if (string.IsNullOrWhiteSpace(releaseId) && !string.IsNullOrWhiteSpace(releaseGroupId))
 208        {
 209            // TODO: Actually try to match the release. Simply taking the first result is stupid.
 210            var releaseGroup = await _musicBrainzQuery.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.None, n
 211            var release = releaseGroup.Releases?.Count > 0 ? releaseGroup.Releases[0] : null;
 212            if (release is not null)
 213            {
 214                releaseId = release.Id.ToString();
 215                result.HasMetadata = true;
 216            }
 217        }
 218
 219        // If there is no release ID, lookup a release with the info we have
 220        if (string.IsNullOrWhiteSpace(releaseId))
 221        {
 222            var artistMusicBrainzId = info.GetMusicBrainzArtistId();
 223            IRelease? releaseResult = null;
 224
 225            if (!string.IsNullOrEmpty(artistMusicBrainzId))
 226            {
 227                var releaseSearchResults = await _musicBrainzQuery.FindReleasesAsync($"\"{info.Name}\" AND arid:{artistM
 228                    .ConfigureAwait(false);
 229                releaseResult = releaseSearchResults.Results.Count > 0 ? releaseSearchResults.Results[0].Item : null;
 230            }
 231            else if (!string.IsNullOrEmpty(info.GetAlbumArtist()))
 232            {
 233                var releaseSearchResults = await _musicBrainzQuery.FindReleasesAsync($"\"{info.Name}\" AND artist:{info.
 234                    .ConfigureAwait(false);
 235                releaseResult = releaseSearchResults.Results.Count > 0 ? releaseSearchResults.Results[0].Item : null;
 236            }
 237
 238            if (releaseResult is not null)
 239            {
 240                releaseId = releaseResult.Id.ToString();
 241
 242                if (releaseResult.ReleaseGroup?.Id is not null)
 243                {
 244                    releaseGroupId = releaseResult.ReleaseGroup.Id.ToString();
 245                }
 246
 247                result.HasMetadata = true;
 248                result.Item.ProductionYear = releaseResult.Date?.Year;
 249                result.Item.Overview = releaseResult.Annotation;
 250            }
 251        }
 252
 253        // If we have a release ID but not a release group ID, lookup the release group
 254        if (!string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId))
 255        {
 256            var release = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.ReleaseGroups, cancell
 257            releaseGroupId = release.ReleaseGroup?.Id.ToString();
 258            result.HasMetadata = true;
 259        }
 260
 261        // If we have a release ID and a release group ID
 262        if (!string.IsNullOrWhiteSpace(releaseId) || !string.IsNullOrWhiteSpace(releaseGroupId))
 263        {
 264            result.HasMetadata = true;
 265        }
 266
 267        if (result.HasMetadata)
 268        {
 269            if (!string.IsNullOrEmpty(releaseId))
 270            {
 271                result.Item.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseId);
 272            }
 273
 274            if (!string.IsNullOrEmpty(releaseGroupId))
 275            {
 276                result.Item.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseGroupId);
 277            }
 278        }
 279
 280        return result;
 281    }
 282
 283    /// <inheritdoc />
 284    public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
 285    {
 0286        throw new NotImplementedException();
 287    }
 288
 289    /// <inheritdoc />
 290    public void Dispose()
 291    {
 21292        Dispose(true);
 21293        GC.SuppressFinalize(this);
 21294    }
 295
 296    /// <summary>
 297    /// Dispose all resources.
 298    /// </summary>
 299    /// <param name="disposing">Whether to dispose.</param>
 300    protected virtual void Dispose(bool disposing)
 301    {
 21302        if (disposing)
 303        {
 21304            _musicBrainzQuery.Dispose();
 305        }
 21306    }
 307}