@ -1,72 +1,42 @@
#nullable disable
#pragma warning disable CS1591, SA1401
using System ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.Globalization ;
using System.IO ;
using System.Linq ;
using System.Net ;
using System.Net.Http ;
using System.Text ;
using System.Threading ;
using System.Threading.Tasks ;
using System.Xml ;
using MediaBrowser.Common.Net ;
using Jellyfin.Extensions ;
using MediaBrowser.Controller.Entities.Audio ;
using MediaBrowser.Controller.Providers ;
using MediaBrowser.Model.Entities ;
using MediaBrowser.Model.Providers ;
using MediaBrowser.Providers.Plugins.MusicBrainz ;
using Microsoft.Extensions.Logging ;
using MediaBrowser.Providers.Music ;
using MetaBrainz.MusicBrainz ;
using MetaBrainz.MusicBrainz.Interfaces.Entities ;
using MetaBrainz.MusicBrainz.Interfaces.Searches ;
namespace MediaBrowser.Providers.Plugins.MusicBrainz ;
namespace MediaBrowser.Providers.Music
/// <summary>
/// Music album metadata provider for MusicBrainz.
/// </summary>
public class MusicBrainzAlbumProvider : IRemoteMetadataProvider < MusicAlbum , AlbumInfo > , IHasOrder , IDisposable
{
public class MusicBrainzAlbumProvider : IRemoteMetadataProvider < MusicAlbum , AlbumInfo > , IHasOrder , IDisposable
{
/// <summary>
/// For each single MB lookup/search, this is the maximum number of
/// attempts that shall be made whilst receiving a 503 Server
/// Unavailable (indicating throttled) response.
/// </summary>
private const uint MusicBrainzQueryAttempts = 5 u ;
private readonly Query _musicBrainzQuery ;
/// <summary>
/// The Jellyfin user-agent is unrestricted but source IP must not exceed
/// one request per second, therefore we rate limit to avoid throttling.
/// Be prudent, use a value slightly above the minimum required.
/// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting.
/// Initializes a new instance of the <see cref="MusicBrainzAlbumProvider"/> class.
/// </summary>
private readonly long _musicBrainzQueryIntervalMs ;
private readonly IHttpClientFactory _httpClientFactory ;
private readonly ILogger < MusicBrainzAlbumProvider > _logger ;
private readonly string _musicBrainzBaseUrl ;
private SemaphoreSlim _apiRequestLock = new SemaphoreSlim ( 1 , 1 ) ;
private Stopwatch _stopWatchMusicBrainz = new Stopwatch ( ) ;
public MusicBrainzAlbumProvider (
IHttpClientFactory httpClientFactory ,
ILogger < MusicBrainzAlbumProvider > logger )
public MusicBrainzAlbumProvider ( )
{
_httpClientFactory = httpClientFactory ;
_logger = logger ;
_musicBrainzBaseUrl = Plugin . Instance . Configuration . Server ;
_musicBrainzQueryIntervalMs = Plugin . Instance . Configuration . RateLimit ;
// Use a stopwatch to ensure we don't exceed the MusicBrainz rate limit
_stopWatchMusicBrainz . Start ( ) ;
MusicBrainz . Plugin . Instance ! . ConfigurationChanged + = ( _ , _ ) = >
{
Query . DefaultServer = MusicBrainz . Plugin . Instance . Configuration . Server ;
Query . DelayBetweenRequests = MusicBrainz . Plugin . Instance . Configuration . RateLimit ;
} ;
Current = this ;
_musicBrainzQuery = new Query ( ) ;
}
internal static MusicBrainzAlbumProvider Current { get ; private set ; }
/// <inheritdoc />
public string Name = > "MusicBrainz" ;
@ -79,101 +49,110 @@ namespace MediaBrowser.Providers.Music
var releaseId = searchInfo . GetReleaseId ( ) ;
var releaseGroupId = searchInfo . GetReleaseGroupId ( ) ;
string url ;
if ( ! string . IsNullOrEmpty ( releaseId ) )
{
url = "/ws/2/release/?query=reid:" + releaseId . ToString ( CultureInfo . InvariantCulture ) ;
var releaseResult = await _musicBrainzQuery . LookupReleaseAsync ( new Guid ( releaseId ) , Include . ReleaseGroups , cancellationToken ) . ConfigureAwait ( false ) ;
return GetReleaseResult ( releaseResult ) . SingleItemAsEnumerable ( ) ;
}
else if ( ! string . IsNullOrEmpty ( releaseGroupId ) )
if ( ! string . IsNullOrEmpty ( releaseGroupId ) )
{
url = "/ws/2/release?release-group=" + releaseGroupId . ToString ( CultureInfo . InvariantCulture ) ;
var releaseGroupResult = await _musicBrainzQuery . LookupReleaseGroupAsync ( new Guid ( releaseGroupId ) , Include . None , null , cancellationToken ) . ConfigureAwait ( false ) ;
return GetReleaseGroupResult ( releaseGroupResult . Releases ) ;
}
else
{
var artistMusicBrainzId = searchInfo . GetMusicBrainzArtistId ( ) ;
if ( ! string . IsNullOrWhiteSpace ( artistMusicBrainzId ) )
{
url = string . Format (
CultureInfo . InvariantCulture ,
"/ws/2/release/?query=\"{0}\" AND arid:{1}" ,
WebUtility . UrlEncode ( searchInfo . Name ) ,
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 ) ;
url = string . Format (
CultureInfo . InvariantCulture ,
"/ws/2/release/?query=\"{0}\" AND artist:\"{1}\"" ,
WebUtility . UrlEncode ( queryName ) ,
WebUtility . UrlEncode ( searchInfo . GetAlbumArtist ( ) ) ) ;
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 ) ;
}
}
if ( ! string . IsNullOrWhiteSpace ( url ) )
return Enumerable . Empty < RemoteSearchResult > ( ) ;
}
private IEnumerable < RemoteSearchResult > GetReleaseSearchResult ( IEnumerable < ISearchResult < IRelease > > ? releaseSearchResults )
{
if ( releaseSearchResults is null )
{
using var response = await GetMusicBrainzResponse ( url , cancellationToken ) . ConfigureAwait ( false ) ;
await using var stream = await response . Content . ReadAsStreamAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
return GetResultsFromResponse ( stream ) ;
yield break ;
}
return Enumerable . Empty < RemoteSearchResult > ( ) ;
foreach ( var result in releaseSearchResults )
{
yield return GetReleaseResult ( result . Item ) ;
}
}
private IEnumerable < RemoteSearchResult > GetResultsFromResponse ( Stream stream )
private IEnumerable < RemoteSearchResult > GetRe leaseGroupResult( IEnumerable < IRelease > ? releaseSearchResults )
{
using var oReader = new StreamReader ( stream , Encoding . UTF8 ) ;
var settings = new XmlReaderSettings ( )
if ( releaseSearchResults is null )
{
ValidationType = ValidationType . None ,
CheckCharacters = false ,
IgnoreProcessingInstructions = true ,
IgnoreComments = true
} ;
yield break ;
}
using var reader = XmlReader . Create ( oReader , settings ) ;
var results = ReleaseResult . Parse ( reader ) ;
foreach ( var result in releaseSearchResults )
{
yield return GetReleaseResult ( result ) ;
}
}
return results . Select ( i = >
private RemoteSearchResult GetReleaseResult ( IRelease releaseSearchResult )
{
var result = new RemoteSearchResult
var sea rchR esult = new RemoteSearchResult
{
Name = i . Title ,
ProductionYear = i . Year
Name = releaseSearchResult . Title ,
ProductionYear = releaseSearchResult . Date ? . Year ,
PremiereDate = releaseSearchResult . Date ? . NearestDate
} ;
if ( i . Artists . Count > 0 )
if ( releaseSearchResult . ArtistCredit ? . Count > 0 )
{
result. AlbumArtist = new RemoteSearchResult
sea rchR esult. AlbumArtist = new RemoteSearchResult
{
SearchProviderName = Name ,
Name = i . Artists [ 0 ] . Item1
Name = releaseSearchResult . ArtistCredit [ 0 ] . Name
} ;
result . AlbumArtist . SetProviderId ( MetadataProvider . MusicBrainzArtist , i . Artists [ 0 ] . Item2 ) ;
}
if ( ! string . IsNullOrWhiteSpace ( i . ReleaseId ) )
if ( releaseSearchResult . ArtistCredit [ 0 ] . Artist ? . Id is not null )
{
result. SetProviderId ( MetadataProvider . MusicBrainzA lbum, i . ReleaseId ) ;
searchResult . AlbumArtist . SetProviderId ( MetadataProvider . MusicBrainzArtist , releaseSearchResult . ArtistCredit [ 0 ] . Artist ! . Id . ToString ( ) ) ;
}
}
searchResult . SetProviderId ( MetadataProvider . MusicBrainzAlbum , releaseSearchResult . Id . ToString ( ) ) ;
if ( ! string . IsNullOrWhiteSpace ( i . ReleaseGroupId ) )
if ( releaseSearchResult . ReleaseGroup ? . Id is not null )
{
result. SetProviderId ( MetadataProvider . MusicBrainzReleaseGroup , i. ReleaseGroupId ) ;
sea rchR esult. SetProviderId ( MetadataProvider . MusicBrainzReleaseGroup , releaseSearchResult. ReleaseGroup . Id . ToString ( ) ) ;
}
return result ;
} ) ;
return searchResult ;
}
/// <inheritdoc />
public async Task < MetadataResult < MusicAlbum > > 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 ( ) ;
@ -182,45 +161,62 @@ namespace MediaBrowser.Providers.Music
Item = new MusicAlbum ( )
} ;
// If we have a release group Id but not a release Id...
// If there is a release group, but no release ID, try to match the release
if ( string . IsNullOrWhiteSpace ( releaseId ) & & ! string . IsNullOrWhiteSpace ( releaseGroupId ) )
{
releaseId = await GetReleaseIdFromReleaseGroupId ( releaseGroupId , cancellationToken ) . ConfigureAwait ( false ) ;
// 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 ! = 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 ;
var releaseResult = await GetReleaseResult ( artistMusicBrainzId , info . GetAlbumArtist ( ) , info . Name , cancellationToken ) . ConfigureAwait ( false ) ;
if ( releaseResult ! = null )
if ( ! string . IsNullOrEmpty ( artistMusicBrainzId ) )
{
if ( ! string . IsNullOrWhiteSpace ( releaseResult . ReleaseId ) )
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 ( ) ) )
{
releaseId = releaseResult . ReleaseId ;
result . HasMetadata = true ;
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 ( ! string . IsNullOrWhiteSpace ( releaseResult . ReleaseGroupId ) )
if ( releaseResult ! = null )
{
releaseGroupId = releaseResult . ReleaseGroupId ;
result . HasMetadata = true ;
releaseId = releaseResult . Id . ToString ( ) ;
if ( releaseResult . ReleaseGroup ? . Id is not null )
{
releaseGroupId = releaseResult . ReleaseGroup . Id . ToString ( ) ;
}
result . Item . ProductionYear = releaseResult . Year ;
result . Item . Overview = releaseResult . Overview ;
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...
// If we have a release ID but not a release group ID, lookup the release group
if ( ! string . IsNullOrWhiteSpace ( releaseId ) & & string . IsNullOrWhiteSpace ( releaseGroupId ) )
{
releaseGroupId = await GetReleaseGroupFromReleaseId ( releaseId , cancellationToken ) . ConfigureAwait ( false ) ;
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 ;
@ -242,378 +238,12 @@ namespace MediaBrowser.Providers.Music
return result ;
}
private Task < ReleaseResult > GetReleaseResult ( string artistMusicBrainId , string artistName , string albumName , CancellationToken cancellationToken )
{
if ( ! string . IsNullOrEmpty ( artistMusicBrainId ) )
{
return GetReleaseResult ( albumName , artistMusicBrainId , cancellationToken ) ;
}
if ( string . IsNullOrWhiteSpace ( artistName ) )
{
return Task . FromResult ( new ReleaseResult ( ) ) ;
}
return GetReleaseResultByArtistName ( albumName , artistName , cancellationToken ) ;
}
private async Task < ReleaseResult > GetReleaseResult ( string albumName , string artistId , CancellationToken cancellationToken )
{
var url = string . Format (
CultureInfo . InvariantCulture ,
"/ws/2/release/?query=\"{0}\" AND arid:{1}" ,
WebUtility . UrlEncode ( albumName ) ,
artistId ) ;
using var response = await GetMusicBrainzResponse ( url , cancellationToken ) . ConfigureAwait ( false ) ;
await using var stream = await response . Content . ReadAsStreamAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
using var oReader = new StreamReader ( stream , Encoding . UTF8 ) ;
var settings = new XmlReaderSettings
{
ValidationType = ValidationType . None ,
CheckCharacters = false ,
IgnoreProcessingInstructions = true ,
IgnoreComments = true
} ;
using var reader = XmlReader . Create ( oReader , settings ) ;
return ReleaseResult . Parse ( reader ) . FirstOrDefault ( ) ;
}
private async Task < ReleaseResult > GetReleaseResultByArtistName ( string albumName , string artistName , CancellationToken cancellationToken )
{
var url = string . Format (
CultureInfo . InvariantCulture ,
"/ws/2/release/?query=\"{0}\" AND artist:\"{1}\"" ,
WebUtility . UrlEncode ( albumName ) ,
WebUtility . UrlEncode ( artistName ) ) ;
using var response = await GetMusicBrainzResponse ( url , cancellationToken ) . ConfigureAwait ( false ) ;
await using var stream = await response . Content . ReadAsStreamAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
using var oReader = new StreamReader ( stream , Encoding . UTF8 ) ;
var settings = new XmlReaderSettings ( )
{
ValidationType = ValidationType . None ,
CheckCharacters = false ,
IgnoreProcessingInstructions = true ,
IgnoreComments = true
} ;
using var reader = XmlReader . Create ( oReader , settings ) ;
return ReleaseResult . Parse ( reader ) . FirstOrDefault ( ) ;
}
private static ( string Name , string ArtistId ) ParseArtistCredit ( XmlReader reader )
{
reader . MoveToContent ( ) ;
reader . Read ( ) ;
// http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator
// Loop through each element
while ( ! reader . EOF & & reader . ReadState = = ReadState . Interactive )
{
if ( reader . NodeType = = XmlNodeType . Element )
{
switch ( reader . Name )
{
case "name-credit" :
{
if ( reader . IsEmptyElement )
{
reader . Read ( ) ;
break ;
}
using var subReader = reader . ReadSubtree ( ) ;
return ParseArtistNameCredit ( subReader ) ;
}
default :
{
reader . Skip ( ) ;
break ;
}
}
}
else
{
reader . Read ( ) ;
}
}
return default ;
}
private static ( string Name , string ArtistId ) ParseArtistNameCredit ( XmlReader reader )
{
reader . MoveToContent ( ) ;
reader . Read ( ) ;
// http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator
// Loop through each element
while ( ! reader . EOF & & reader . ReadState = = ReadState . Interactive )
{
if ( reader . NodeType = = XmlNodeType . Element )
{
switch ( reader . Name )
{
case "artist" :
{
if ( reader . IsEmptyElement )
{
reader . Read ( ) ;
break ;
}
var id = reader . GetAttribute ( "id" ) ;
using var subReader = reader . ReadSubtree ( ) ;
return ParseArtistArtistCredit ( subReader , id ) ;
}
default :
{
reader . Skip ( ) ;
break ;
}
}
}
else
{
reader . Read ( ) ;
}
}
return ( null , null ) ;
}
private static ( string Name , string ArtistId ) ParseArtistArtistCredit ( XmlReader reader , string artistId )
{
reader . MoveToContent ( ) ;
reader . Read ( ) ;
string name = null ;
// http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator
// Loop through each element
while ( ! reader . EOF & & reader . ReadState = = ReadState . Interactive )
{
if ( reader . NodeType = = XmlNodeType . Element )
{
switch ( reader . Name )
{
case "name" :
{
name = reader . ReadElementContentAsString ( ) ;
break ;
}
default :
{
reader . Skip ( ) ;
break ;
}
}
}
else
{
reader . Read ( ) ;
}
}
return ( name , artistId ) ;
}
private async Task < string > GetReleaseIdFromReleaseGroupId ( string releaseGroupId , CancellationToken cancellationToken )
{
var url = "/ws/2/release?release-group=" + releaseGroupId . ToString ( CultureInfo . InvariantCulture ) ;
using var response = await GetMusicBrainzResponse ( url , cancellationToken ) . ConfigureAwait ( false ) ;
await using var stream = await response . Content . ReadAsStreamAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
using var oReader = new StreamReader ( stream , Encoding . UTF8 ) ;
var settings = new XmlReaderSettings
{
ValidationType = ValidationType . None ,
CheckCharacters = false ,
IgnoreProcessingInstructions = true ,
IgnoreComments = true
} ;
using var reader = XmlReader . Create ( oReader , settings ) ;
var result = ReleaseResult . Parse ( reader ) . FirstOrDefault ( ) ;
return result ? . ReleaseId ;
}
/// <summary>
/// Gets the release group id internal.
/// </summary>
/// <param name="releaseEntryId">The release entry id.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{System.String}.</returns>
private async Task < string > GetReleaseGroupFromReleaseId ( string releaseEntryId , CancellationToken cancellationToken )
{
var url = "/ws/2/release-group/?query=reid:" + releaseEntryId . ToString ( CultureInfo . InvariantCulture ) ;
using var response = await GetMusicBrainzResponse ( url , cancellationToken ) . ConfigureAwait ( false ) ;
await using var stream = await response . Content . ReadAsStreamAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
using var oReader = new StreamReader ( stream , Encoding . UTF8 ) ;
var settings = new XmlReaderSettings
{
ValidationType = ValidationType . None ,
CheckCharacters = false ,
IgnoreProcessingInstructions = true ,
IgnoreComments = true ,
Async = true
} ;
using var reader = XmlReader . Create ( oReader , settings ) ;
await reader . MoveToContentAsync ( ) . ConfigureAwait ( false ) ;
await reader . ReadAsync ( ) . ConfigureAwait ( false ) ;
// Loop through each element
while ( ! reader . EOF & & reader . ReadState = = ReadState . Interactive )
{
if ( reader . NodeType = = XmlNodeType . Element )
{
switch ( reader . Name )
{
case "release-group-list" :
{
if ( reader . IsEmptyElement )
{
await reader . ReadAsync ( ) . ConfigureAwait ( false ) ;
continue ;
}
using var subReader = reader . ReadSubtree ( ) ;
return GetFirstReleaseGroupId ( subReader ) ;
}
default :
{
await reader . SkipAsync ( ) . ConfigureAwait ( false ) ;
break ;
}
}
}
else
{
await reader . ReadAsync ( ) . ConfigureAwait ( false ) ;
}
}
return null ;
}
private string GetFirstReleaseGroupId ( XmlReader reader )
{
reader . MoveToContent ( ) ;
reader . Read ( ) ;
// Loop through each element
while ( ! reader . EOF & & reader . ReadState = = ReadState . Interactive )
{
if ( reader . NodeType = = XmlNodeType . Element )
{
switch ( reader . Name )
{
case "release-group" :
{
return reader . GetAttribute ( "id" ) ;
}
default :
{
reader . Skip ( ) ;
break ;
}
}
}
else
{
reader . Read ( ) ;
}
}
return null ;
}
/// <summary>
/// Makes request to MusicBrainz server and awaits a response.
/// A 503 Service Unavailable response indicates throttling to maintain a rate limit.
/// A number of retries shall be made in order to try and satisfy the request before
/// giving up and returning null.
/// </summary>
/// <param name="url">Address of MusicBrainz server.</param>
/// <param name="cancellationToken">CancellationToken to use for method.</param>
/// <returns>Returns response from MusicBrainz service.</returns>
internal async Task < HttpResponseMessage > GetMusicBrainzResponse ( string url , CancellationToken cancellationToken )
{
await _apiRequestLock . WaitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
try
{
HttpResponseMessage response ;
var attempts = 0 u ;
var requestUrl = _musicBrainzBaseUrl . TrimEnd ( '/' ) + url ;
do
{
attempts + + ;
if ( _stopWatchMusicBrainz . ElapsedMilliseconds < _musicBrainzQueryIntervalMs )
{
// MusicBrainz is extremely adamant about limiting to one request per second.
var delayMs = _musicBrainzQueryIntervalMs - _stopWatchMusicBrainz . ElapsedMilliseconds ;
await Task . Delay ( ( int ) delayMs , cancellationToken ) . ConfigureAwait ( false ) ;
}
// Write time since last request to debug log as evidence we're meeting rate limit
// requirement, before resetting stopwatch back to zero.
_logger . LogDebug ( "GetMusicBrainzResponse: Time since previous request: {0} ms" , _stopWatchMusicBrainz . ElapsedMilliseconds ) ;
_stopWatchMusicBrainz . Restart ( ) ;
using var request = new HttpRequestMessage ( HttpMethod . Get , requestUrl ) ;
response = await _httpClientFactory
. CreateClient ( NamedClient . MusicBrainz )
. SendAsync ( request , cancellationToken )
. ConfigureAwait ( false ) ;
// We retry a finite number of times, and only whilst MB is indicating 503 (throttling).
}
while ( attempts < MusicBrainzQueryAttempts & & response . StatusCode = = HttpStatusCode . ServiceUnavailable ) ;
// Log error if unable to query MB database due to throttling.
if ( attempts = = MusicBrainzQueryAttempts & & response . StatusCode = = HttpStatusCode . ServiceUnavailable )
{
_logger . LogError ( "GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}" , attempts , requestUrl ) ;
}
return response ;
}
finally
{
_apiRequestLock . Release ( ) ;
}
}
/// <inheritdoc />
public Task < HttpResponseMessage > GetImageResponse ( string url , CancellationToken cancellationToken )
{
throw new NotImplementedException ( ) ;
}
protected virtual void Dispose ( bool disposing )
{
if ( disposing )
{
_apiRequestLock ? . Dispose ( ) ;
}
}
/// <inheritdoc />
public void Dispose ( )
{
@ -621,185 +251,15 @@ namespace MediaBrowser.Providers.Music
GC . SuppressFinalize ( this ) ;
}
private class ReleaseResult
{
public string ReleaseId ;
public string ReleaseGroupId ;
public string Title ;
public string Overview ;
public int? Year ;
public List < ( string , string ) > Artists = new ( ) ;
public static IEnumerable < ReleaseResult > Parse ( XmlReader reader )
{
reader . MoveToContent ( ) ;
reader . Read ( ) ;
// Loop through each element
while ( ! reader . EOF & & reader . ReadState = = ReadState . Interactive )
{
if ( reader . NodeType = = XmlNodeType . Element )
{
switch ( reader . Name )
{
case "release-list" :
{
if ( reader . IsEmptyElement )
{
reader . Read ( ) ;
continue ;
}
using var subReader = reader . ReadSubtree ( ) ;
return ParseReleaseList ( subReader ) . ToList ( ) ;
}
default :
{
reader . Skip ( ) ;
break ;
}
}
}
else
{
reader . Read ( ) ;
}
}
return Enumerable . Empty < ReleaseResult > ( ) ;
}
private static IEnumerable < ReleaseResult > ParseReleaseList ( XmlReader reader )
{
reader . MoveToContent ( ) ;
reader . Read ( ) ;
// Loop through each element
while ( ! reader . EOF & & reader . ReadState = = ReadState . Interactive )
{
if ( reader . NodeType = = XmlNodeType . Element )
{
switch ( reader . Name )
{
case "release" :
{
if ( reader . IsEmptyElement )
{
reader . Read ( ) ;
continue ;
}
var releaseId = reader . GetAttribute ( "id" ) ;
using var subReader = reader . ReadSubtree ( ) ;
var release = ParseRelease ( subReader , releaseId ) ;
if ( release ! = null )
{
yield return release ;
}
break ;
}
default :
{
reader . Skip ( ) ;
break ;
}
}
}
else
{
reader . Read ( ) ;
}
}
}
private static ReleaseResult ParseRelease ( XmlReader reader , string releaseId )
{
var result = new ReleaseResult
{
ReleaseId = releaseId
} ;
reader . MoveToContent ( ) ;
reader . Read ( ) ;
// http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator
// Loop through each element
while ( ! reader . EOF & & reader . ReadState = = ReadState . Interactive )
{
if ( reader . NodeType = = XmlNodeType . Element )
{
switch ( reader . Name )
{
case "title" :
{
result . Title = reader . ReadElementContentAsString ( ) ;
break ;
}
case "date" :
{
var val = reader . ReadElementContentAsString ( ) ;
if ( DateTime . TryParse ( val , out var date ) )
{
result . Year = date . Year ;
}
break ;
}
case "annotation" :
{
result . Overview = reader . ReadElementContentAsString ( ) ;
break ;
}
case "release-group" :
{
result . ReleaseGroupId = reader . GetAttribute ( "id" ) ;
reader . Skip ( ) ;
break ;
}
case "artist-credit" :
{
if ( reader . IsEmptyElement )
{
reader . Read ( ) ;
break ;
}
using var subReader = reader . ReadSubtree ( ) ;
var artist = ParseArtistCredit ( subReader ) ;
if ( ! string . IsNullOrEmpty ( artist . Name ) )
{
result . Artists . Add ( artist ) ;
}
break ;
}
default :
/// <summary>
/// Dispose all resources.
/// </summary>
/// <param name="disposing">Whether to dispose.</param>
protected virtual void Dispose ( bool disposing )
{
reader . Skip ( ) ;
break ;
}
}
}
else
if ( disposing )
{
reader . Read ( ) ;
}
}
return result ;
}
_musicBrainzQuery . Dispose ( ) ;
}
}
}