2019-01-13 21:03:10 +01:00
using System ;
2014-02-20 05:53:15 +01:00
using System.Collections.Generic ;
2019-03-13 21:31:21 +01:00
using System.Diagnostics ;
2019-09-10 22:37:53 +02:00
using System.Globalization ;
2013-11-12 18:12:11 +01:00
using System.IO ;
2014-03-14 04:23:58 +01:00
using System.Linq ;
2013-11-12 18:12:11 +01:00
using System.Net ;
using System.Text ;
using System.Threading ;
using System.Threading.Tasks ;
using System.Xml ;
2019-01-13 20:26:31 +01:00
using MediaBrowser.Common ;
using MediaBrowser.Common.Net ;
using MediaBrowser.Controller.Entities.Audio ;
using MediaBrowser.Controller.Providers ;
using MediaBrowser.Model.Entities ;
using MediaBrowser.Model.Providers ;
2020-02-22 07:04:52 +01:00
using MediaBrowser.Providers.Plugins.MusicBrainz ;
2019-01-13 20:26:31 +01:00
using Microsoft.Extensions.Logging ;
2013-11-12 18:12:11 +01:00
namespace MediaBrowser.Providers.Music
{
2014-02-07 04:10:13 +01:00
public class MusicBrainzAlbumProvider : IRemoteMetadataProvider < MusicAlbum , AlbumInfo > , IHasOrder
2013-11-12 18:12:11 +01:00
{
2019-03-15 20:29:04 +01:00
/// <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 minimun required.
/// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting
/// </summary>
2020-02-23 16:25:27 +01:00
private readonly long _musicBrainzQueryIntervalMs ;
2019-03-14 01:43:41 +01:00
2019-03-15 20:29:04 +01:00
/// <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 ;
2019-09-10 22:37:53 +02:00
internal static MusicBrainzAlbumProvider Current ;
private readonly IHttpClient _httpClient ;
private readonly IApplicationHost _appHost ;
private readonly ILogger _logger ;
private readonly string _musicBrainzBaseUrl ;
private Stopwatch _stopWatchMusicBrainz = new Stopwatch ( ) ;
2019-03-08 17:15:52 +01:00
public MusicBrainzAlbumProvider (
IHttpClient httpClient ,
IApplicationHost appHost ,
2020-02-22 07:04:52 +01:00
ILogger logger )
2013-11-12 18:12:11 +01:00
{
_httpClient = httpClient ;
2014-01-29 02:45:48 +01:00
_appHost = appHost ;
2014-09-04 03:44:40 +02:00
_logger = logger ;
2019-03-12 16:37:18 +01:00
2020-02-22 07:04:52 +01:00
_musicBrainzBaseUrl = Plugin . Instance . Configuration . Server ;
_musicBrainzQueryIntervalMs = Plugin . Instance . Configuration . RateLimit ;
2019-03-12 16:37:18 +01:00
2019-03-13 21:31:21 +01:00
// Use a stopwatch to ensure we don't exceed the MusicBrainz rate limit
_stopWatchMusicBrainz . Start ( ) ;
2013-11-12 18:12:11 +01:00
Current = this ;
}
2019-09-10 22:37:53 +02:00
/// <inheritdoc />
public string Name = > "MusicBrainz" ;
/// <inheritdoc />
public int Order = > 0 ;
/// <inheritdoc />
2014-02-20 05:53:15 +01:00
public async Task < IEnumerable < RemoteSearchResult > > GetSearchResults ( AlbumInfo searchInfo , CancellationToken cancellationToken )
{
2020-02-22 07:04:52 +01:00
// TODO maybe remove when artist metadata can be disabled
if ( ! Plugin . Instance . Configuration . Enable )
{
return Enumerable . Empty < RemoteSearchResult > ( ) ;
}
2014-03-14 04:23:58 +01:00
var releaseId = searchInfo . GetReleaseId ( ) ;
2017-03-15 20:57:18 +01:00
var releaseGroupId = searchInfo . GetReleaseGroupId ( ) ;
2014-03-14 04:23:58 +01:00
2019-03-13 22:32:52 +01:00
string url ;
2014-03-14 04:23:58 +01:00
if ( ! string . IsNullOrEmpty ( releaseId ) )
{
2019-09-10 22:37:53 +02:00
url = "/ws/2/release/?query=reid:" + releaseId . ToString ( CultureInfo . InvariantCulture ) ;
2014-03-14 04:23:58 +01:00
}
2017-03-15 20:57:18 +01:00
else if ( ! string . IsNullOrEmpty ( releaseGroupId ) )
{
2019-09-10 22:37:53 +02:00
url = "/ws/2/release?release-group=" + releaseGroupId . ToString ( CultureInfo . InvariantCulture ) ;
2017-03-15 20:57:18 +01:00
}
2014-03-14 04:23:58 +01:00
else
{
var artistMusicBrainzId = searchInfo . GetMusicBrainzArtistId ( ) ;
if ( ! string . IsNullOrWhiteSpace ( artistMusicBrainzId ) )
{
2019-09-10 22:37:53 +02:00
url = string . Format (
CultureInfo . InvariantCulture ,
"/ws/2/release/?query=\"{0}\" AND arid:{1}" ,
2014-03-14 04:23:58 +01:00
WebUtility . UrlEncode ( searchInfo . Name ) ,
artistMusicBrainzId ) ;
}
else
{
2018-09-12 19:26:21 +02:00
// I'm sure there is a better way but for now it resolves search for 12" Mixes
var queryName = searchInfo . Name . Replace ( "\"" , string . Empty ) ;
2019-09-10 22:37:53 +02:00
url = string . Format (
CultureInfo . InvariantCulture ,
"/ws/2/release/?query=\"{0}\" AND artist:\"{1}\"" ,
2020-02-22 07:04:52 +01:00
WebUtility . UrlEncode ( queryName ) ,
WebUtility . UrlEncode ( searchInfo . GetAlbumArtist ( ) ) ) ;
2014-03-14 04:23:58 +01:00
}
}
if ( ! string . IsNullOrWhiteSpace ( url ) )
{
2019-03-13 21:31:21 +01:00
using ( var response = await GetMusicBrainzResponse ( url , cancellationToken ) . ConfigureAwait ( false ) )
2019-09-10 22:37:53 +02:00
using ( var stream = response . Content )
2016-10-27 21:03:23 +02:00
{
2019-09-10 22:37:53 +02:00
return GetResultsFromResponse ( stream ) ;
2016-10-27 21:03:23 +02:00
}
2014-03-14 04:23:58 +01:00
}
2019-03-13 22:32:52 +01:00
return Enumerable . Empty < RemoteSearchResult > ( ) ;
2014-02-20 05:53:15 +01:00
}
2019-03-13 22:32:52 +01:00
private IEnumerable < RemoteSearchResult > GetResultsFromResponse ( Stream stream )
2014-03-14 04:23:58 +01:00
{
2016-10-27 23:05:25 +02:00
using ( var oReader = new StreamReader ( stream , Encoding . UTF8 ) )
2014-03-14 04:23:58 +01:00
{
2019-02-01 17:43:31 +01:00
var settings = new XmlReaderSettings ( )
{
ValidationType = ValidationType . None ,
CheckCharacters = false ,
IgnoreProcessingInstructions = true ,
IgnoreComments = true
} ;
2016-10-27 23:05:25 +02:00
using ( var reader = XmlReader . Create ( oReader , settings ) )
2016-08-16 20:45:57 +02:00
{
2016-10-27 23:05:25 +02:00
var results = ReleaseResult . Parse ( reader ) ;
2014-03-14 04:23:58 +01:00
2016-10-27 23:05:25 +02:00
return results . Select ( i = >
{
var result = new RemoteSearchResult
{
Name = i . Title ,
ProductionYear = i . Year
} ;
2017-10-28 06:20:18 +02:00
if ( i . Artists . Count > 0 )
{
result . AlbumArtist = new RemoteSearchResult
{
SearchProviderName = Name ,
Name = i . Artists [ 0 ] . Item1
} ;
result . AlbumArtist . SetProviderId ( MetadataProviders . MusicBrainzArtist , i . Artists [ 0 ] . Item2 ) ;
}
2016-10-27 23:05:25 +02:00
if ( ! string . IsNullOrWhiteSpace ( i . ReleaseId ) )
{
result . SetProviderId ( MetadataProviders . MusicBrainzAlbum , i . ReleaseId ) ;
}
2019-09-10 22:37:53 +02:00
2016-10-27 23:05:25 +02:00
if ( ! string . IsNullOrWhiteSpace ( i . ReleaseGroupId ) )
{
result . SetProviderId ( MetadataProviders . MusicBrainzReleaseGroup , i . ReleaseGroupId ) ;
}
return result ;
2019-03-13 22:32:52 +01:00
} ) ;
2016-10-27 23:05:25 +02:00
}
}
2014-03-14 04:23:58 +01:00
}
2019-09-10 22:37:53 +02:00
/// <inheritdoc />
2014-02-07 04:10:13 +01:00
public async Task < MetadataResult < MusicAlbum > > GetMetadata ( AlbumInfo id , CancellationToken cancellationToken )
2013-11-12 18:12:11 +01:00
{
2014-02-07 23:40:03 +01:00
var releaseId = id . GetReleaseId ( ) ;
var releaseGroupId = id . GetReleaseGroupId ( ) ;
2013-11-12 18:12:11 +01:00
2014-02-07 23:40:03 +01:00
var result = new MetadataResult < MusicAlbum >
{
Item = new MusicAlbum ( )
} ;
2013-11-12 18:12:11 +01:00
2020-02-22 07:04:52 +01:00
// TODO maybe remove when artist metadata can be disabled
if ( ! Plugin . Instance . Configuration . Enable )
{
return result ;
}
2017-03-15 20:57:18 +01:00
// If we have a release group Id but not a release Id...
if ( string . IsNullOrWhiteSpace ( releaseId ) & & ! string . IsNullOrWhiteSpace ( releaseGroupId ) )
{
releaseId = await GetReleaseIdFromReleaseGroupId ( releaseGroupId , cancellationToken ) . ConfigureAwait ( false ) ;
result . HasMetadata = true ;
}
if ( string . IsNullOrWhiteSpace ( releaseId ) )
2013-11-12 18:12:11 +01:00
{
2014-02-09 08:27:44 +01:00
var artistMusicBrainzId = id . GetMusicBrainzArtistId ( ) ;
2013-11-12 18:12:11 +01:00
2014-02-07 23:40:03 +01:00
var releaseResult = await GetReleaseResult ( artistMusicBrainzId , id . GetAlbumArtist ( ) , id . Name , cancellationToken ) . ConfigureAwait ( false ) ;
2014-01-31 20:55:21 +01:00
2016-10-08 20:51:07 +02:00
if ( releaseResult ! = null )
2013-11-12 18:12:11 +01:00
{
2017-03-15 20:57:18 +01:00
if ( ! string . IsNullOrWhiteSpace ( releaseResult . ReleaseId ) )
2016-10-08 20:51:07 +02:00
{
releaseId = releaseResult . ReleaseId ;
result . HasMetadata = true ;
}
2013-11-12 18:12:11 +01:00
2017-03-15 20:57:18 +01:00
if ( ! string . IsNullOrWhiteSpace ( releaseResult . ReleaseGroupId ) )
2016-10-08 20:51:07 +02:00
{
releaseGroupId = releaseResult . ReleaseGroupId ;
result . HasMetadata = true ;
}
result . Item . ProductionYear = releaseResult . Year ;
result . Item . Overview = releaseResult . Overview ;
2013-11-12 18:12:11 +01:00
}
}
// If we have a release Id but not a release group Id...
2017-03-15 20:57:18 +01:00
if ( ! string . IsNullOrWhiteSpace ( releaseId ) & & string . IsNullOrWhiteSpace ( releaseGroupId ) )
2013-11-12 18:12:11 +01:00
{
2017-03-15 20:57:18 +01:00
releaseGroupId = await GetReleaseGroupFromReleaseId ( releaseId , cancellationToken ) . ConfigureAwait ( false ) ;
2014-01-31 20:55:21 +01:00
result . HasMetadata = true ;
2013-11-12 18:12:11 +01:00
}
2017-03-15 20:57:18 +01:00
if ( ! string . IsNullOrWhiteSpace ( releaseId ) | | ! string . IsNullOrWhiteSpace ( releaseGroupId ) )
2015-08-07 16:21:29 +02:00
{
result . HasMetadata = true ;
}
2014-02-07 23:40:03 +01:00
if ( result . HasMetadata )
{
if ( ! string . IsNullOrEmpty ( releaseId ) )
{
result . Item . SetProviderId ( MetadataProviders . MusicBrainzAlbum , releaseId ) ;
}
if ( ! string . IsNullOrEmpty ( releaseGroupId ) )
{
result . Item . SetProviderId ( MetadataProviders . MusicBrainzReleaseGroup , releaseGroupId ) ;
}
}
2014-01-31 20:55:21 +01:00
return result ;
2013-11-12 18:12:11 +01:00
}
2014-01-31 20:55:21 +01:00
private Task < ReleaseResult > GetReleaseResult ( string artistMusicBrainId , string artistName , string albumName , CancellationToken cancellationToken )
{
if ( ! string . IsNullOrEmpty ( artistMusicBrainId ) )
2013-11-12 18:12:11 +01:00
{
2014-01-31 20:55:21 +01:00
return GetReleaseResult ( albumName , artistMusicBrainId , cancellationToken ) ;
2013-11-12 18:12:11 +01:00
}
2014-06-23 18:05:19 +02:00
if ( string . IsNullOrWhiteSpace ( artistName ) )
{
return Task . FromResult ( new ReleaseResult ( ) ) ;
}
2014-01-31 20:55:21 +01:00
return GetReleaseResultByArtistName ( albumName , artistName , cancellationToken ) ;
2013-11-12 18:12:11 +01:00
}
private async Task < ReleaseResult > GetReleaseResult ( string albumName , string artistId , CancellationToken cancellationToken )
{
2016-06-15 21:52:38 +02:00
var url = string . Format ( "/ws/2/release/?query=\"{0}\" AND arid:{1}" ,
2013-11-12 18:12:11 +01:00
WebUtility . UrlEncode ( albumName ) ,
artistId ) ;
2019-03-13 21:31:21 +01:00
using ( var response = await GetMusicBrainzResponse ( url , cancellationToken ) . ConfigureAwait ( false ) )
2019-02-01 17:43:31 +01:00
using ( var stream = response . Content )
using ( var oReader = new StreamReader ( stream , Encoding . UTF8 ) )
2016-10-27 21:03:23 +02:00
{
2019-02-01 17:43:31 +01:00
var settings = new XmlReaderSettings ( )
2016-10-27 23:05:25 +02:00
{
2019-02-01 17:43:31 +01:00
ValidationType = ValidationType . None ,
CheckCharacters = false ,
IgnoreProcessingInstructions = true ,
IgnoreComments = true
} ;
2016-10-27 23:05:25 +02:00
2019-02-01 17:43:31 +01:00
using ( var reader = XmlReader . Create ( oReader , settings ) )
{
return ReleaseResult . Parse ( reader ) . FirstOrDefault ( ) ;
2016-10-27 23:05:25 +02:00
}
2016-10-27 21:03:23 +02:00
}
2013-11-12 18:12:11 +01:00
}
private async Task < ReleaseResult > GetReleaseResultByArtistName ( string albumName , string artistName , CancellationToken cancellationToken )
{
2019-09-10 22:37:53 +02:00
var url = string . Format (
CultureInfo . InvariantCulture ,
"/ws/2/release/?query=\"{0}\" AND artist:\"{1}\"" ,
2013-11-12 18:12:11 +01:00
WebUtility . UrlEncode ( albumName ) ,
WebUtility . UrlEncode ( artistName ) ) ;
2019-03-13 21:31:21 +01:00
using ( var response = await GetMusicBrainzResponse ( url , cancellationToken ) . ConfigureAwait ( false ) )
2019-02-01 17:43:31 +01:00
using ( var stream = response . Content )
using ( var oReader = new StreamReader ( stream , Encoding . UTF8 ) )
2016-10-27 21:03:23 +02:00
{
2019-02-01 17:43:31 +01:00
var settings = new XmlReaderSettings ( )
2016-10-27 23:05:25 +02:00
{
2019-02-01 17:43:31 +01:00
ValidationType = ValidationType . None ,
CheckCharacters = false ,
IgnoreProcessingInstructions = true ,
IgnoreComments = true
} ;
2016-10-27 23:05:25 +02:00
2019-02-01 17:43:31 +01:00
using ( var reader = XmlReader . Create ( oReader , settings ) )
{
return ReleaseResult . Parse ( reader ) . FirstOrDefault ( ) ;
2016-10-27 23:05:25 +02:00
}
2016-10-27 21:03:23 +02:00
}
2013-11-12 18:12:11 +01:00
}
2016-06-15 21:52:38 +02:00
private class ReleaseResult
2013-11-12 18:12:11 +01:00
{
2016-06-15 21:52:38 +02:00
public string ReleaseId ;
public string ReleaseGroupId ;
2016-08-16 20:45:57 +02:00
public string Title ;
2016-10-08 20:51:07 +02:00
public string Overview ;
public int? Year ;
2013-11-12 18:12:11 +01:00
2018-09-12 19:26:21 +02:00
public List < ValueTuple < string , string > > Artists = new List < ValueTuple < string , string > > ( ) ;
2017-10-28 06:20:18 +02:00
2019-03-13 22:32:52 +01:00
public static IEnumerable < ReleaseResult > Parse ( XmlReader reader )
2013-11-12 18:12:11 +01:00
{
2016-10-27 21:03:23 +02:00
reader . MoveToContent ( ) ;
2016-11-02 18:08:20 +01:00
reader . Read ( ) ;
2013-11-12 18:12:11 +01:00
2016-10-27 21:03:23 +02:00
// Loop through each element
2016-12-03 22:46:06 +01:00
while ( ! reader . EOF & & reader . ReadState = = ReadState . Interactive )
2016-06-15 21:52:38 +02:00
{
2016-10-27 23:05:25 +02:00
if ( reader . NodeType = = XmlNodeType . Element )
2016-06-15 21:52:38 +02:00
{
2016-10-27 23:05:25 +02:00
switch ( reader . Name )
{
case "release-list" :
2016-10-27 21:03:23 +02:00
{
2016-12-04 00:57:34 +01:00
if ( reader . IsEmptyElement )
{
reader . Read ( ) ;
continue ;
}
2019-09-10 22:37:53 +02:00
2016-10-27 23:05:25 +02:00
using ( var subReader = reader . ReadSubtree ( ) )
{
2019-04-30 22:18:40 +02:00
return ParseReleaseList ( subReader ) . ToList ( ) ;
2016-10-27 23:05:25 +02:00
}
2016-10-27 21:03:23 +02:00
}
2016-10-27 23:05:25 +02:00
default :
{
reader . Skip ( ) ;
break ;
}
}
2016-06-15 21:52:38 +02:00
}
2016-10-31 19:59:58 +01:00
else
{
reader . Read ( ) ;
}
2016-06-15 21:52:38 +02:00
}
2013-11-12 18:12:11 +01:00
2019-03-13 22:32:52 +01:00
return Enumerable . Empty < ReleaseResult > ( ) ;
2016-08-16 20:45:57 +02:00
}
2019-03-13 22:32:52 +01:00
private static IEnumerable < ReleaseResult > ParseReleaseList ( XmlReader reader )
2016-10-08 20:51:07 +02:00
{
2016-10-27 21:03:23 +02:00
reader . MoveToContent ( ) ;
2016-11-02 18:08:20 +01:00
reader . Read ( ) ;
2016-10-27 21:03:23 +02:00
// Loop through each element
2016-12-03 22:46:06 +01:00
while ( ! reader . EOF & & reader . ReadState = = ReadState . Interactive )
2016-10-08 20:51:07 +02:00
{
2016-10-27 23:05:25 +02:00
if ( reader . NodeType = = XmlNodeType . Element )
2016-10-08 20:51:07 +02:00
{
2016-10-27 23:05:25 +02:00
switch ( reader . Name )
{
case "release" :
2016-10-27 21:03:23 +02:00
{
2016-12-04 00:57:34 +01:00
if ( reader . IsEmptyElement )
{
reader . Read ( ) ;
continue ;
}
2016-10-28 00:55:56 +02:00
var releaseId = reader . GetAttribute ( "id" ) ;
2016-10-27 23:05:25 +02:00
using ( var subReader = reader . ReadSubtree ( ) )
{
var release = ParseRelease ( subReader , releaseId ) ;
if ( release ! = null )
{
2019-03-13 22:32:52 +01:00
yield return release ;
2016-10-27 23:05:25 +02:00
}
}
break ;
2016-10-27 21:03:23 +02:00
}
2016-10-27 23:05:25 +02:00
default :
{
reader . Skip ( ) ;
break ;
}
}
2016-10-08 20:51:07 +02:00
}
2016-10-31 19:59:58 +01:00
else
{
reader . Read ( ) ;
}
2016-10-08 20:51:07 +02:00
}
}
2016-10-27 21:03:23 +02:00
private static ReleaseResult ParseRelease ( XmlReader reader , string releaseId )
2016-08-16 20:45:57 +02:00
{
2016-10-27 21:03:23 +02:00
var result = new ReleaseResult
2016-06-15 21:52:38 +02:00
{
2016-10-27 21:03:23 +02:00
ReleaseId = releaseId
} ;
2016-08-16 20:45:57 +02:00
2016-10-27 21:03:23 +02:00
reader . MoveToContent ( ) ;
2016-10-27 23:05:25 +02:00
reader . Read ( ) ;
// http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator
2016-10-27 21:03:23 +02:00
// Loop through each element
2016-12-03 22:46:06 +01:00
while ( ! reader . EOF & & reader . ReadState = = ReadState . Interactive )
2016-06-15 21:52:38 +02:00
{
2016-10-27 23:05:25 +02:00
if ( reader . NodeType = = XmlNodeType . Element )
2016-06-15 21:52:38 +02:00
{
2016-10-27 23:05:25 +02:00
switch ( reader . Name )
{
case "title" :
2016-10-27 21:03:23 +02:00
{
2016-10-27 23:05:25 +02:00
result . Title = reader . ReadElementContentAsString ( ) ;
break ;
2016-10-27 21:03:23 +02:00
}
2016-10-27 23:05:25 +02:00
case "date" :
{
var val = reader . ReadElementContentAsString ( ) ;
2019-01-13 21:46:33 +01:00
if ( DateTime . TryParse ( val , out var date ) )
2016-10-27 23:05:25 +02:00
{
result . Year = date . Year ;
}
break ;
}
case "annotation" :
{
result . Overview = reader . ReadElementContentAsString ( ) ;
break ;
}
case "release-group" :
{
result . ReleaseGroupId = reader . GetAttribute ( "id" ) ;
2016-11-04 03:33:47 +01:00
reader . Skip ( ) ;
2017-10-28 06:20:18 +02:00
break ;
}
case "artist-credit" :
{
using ( var subReader = reader . ReadSubtree ( ) )
{
var artist = ParseArtistCredit ( subReader ) ;
2018-09-12 19:26:21 +02:00
if ( ! string . IsNullOrEmpty ( artist . Item1 ) )
2017-10-28 06:20:18 +02:00
{
result . Artists . Add ( artist ) ;
}
}
2016-10-27 23:05:25 +02:00
break ;
}
default :
2016-10-28 00:55:56 +02:00
{
reader . Skip ( ) ;
break ;
}
2016-10-27 23:05:25 +02:00
}
}
else
{
reader . Read ( ) ;
2016-06-15 21:52:38 +02:00
}
}
2013-11-12 18:12:11 +01:00
2016-10-27 21:03:23 +02:00
return result ;
2016-06-15 21:52:38 +02:00
}
2013-11-12 18:12:11 +01:00
}
2018-09-12 19:26:21 +02:00
private static ValueTuple < string , string > ParseArtistCredit ( XmlReader reader )
2017-10-28 06:20:18 +02:00
{
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" :
{
using ( var subReader = reader . ReadSubtree ( ) )
{
return ParseArtistNameCredit ( subReader ) ;
}
}
default :
{
reader . Skip ( ) ;
break ;
}
}
}
else
{
reader . Read ( ) ;
}
}
2018-09-12 19:26:21 +02:00
return new ValueTuple < string , string > ( ) ;
2017-10-28 06:20:18 +02:00
}
2019-01-25 21:52:10 +01:00
private static ( string , string ) ParseArtistNameCredit ( XmlReader reader )
2017-10-28 06:20:18 +02:00
{
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" :
{
var id = reader . GetAttribute ( "id" ) ;
using ( var subReader = reader . ReadSubtree ( ) )
{
return ParseArtistArtistCredit ( subReader , id ) ;
}
}
default :
{
reader . Skip ( ) ;
break ;
}
}
}
else
{
reader . Read ( ) ;
}
}
2019-01-25 21:52:10 +01:00
return ( null , null ) ;
2017-10-28 06:20:18 +02:00
}
2019-03-13 22:32:52 +01:00
private static ( string name , string id ) ParseArtistArtistCredit ( XmlReader reader , string artistId )
2017-10-28 06:20:18 +02:00
{
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 ( ) ;
}
}
2019-03-13 22:32:52 +01:00
return ( name , artistId ) ;
2017-10-28 06:20:18 +02:00
}
2017-03-15 20:57:18 +01:00
private async Task < string > GetReleaseIdFromReleaseGroupId ( string releaseGroupId , CancellationToken cancellationToken )
{
2019-09-10 22:37:53 +02:00
var url = "/ws/2/release?release-group=" + releaseGroupId . ToString ( CultureInfo . InvariantCulture ) ;
2017-03-15 20:57:18 +01:00
2019-03-13 21:31:21 +01:00
using ( var response = await GetMusicBrainzResponse ( url , cancellationToken ) . ConfigureAwait ( false ) )
2019-02-01 17:43:31 +01:00
using ( var stream = response . Content )
using ( var oReader = new StreamReader ( stream , Encoding . UTF8 ) )
2017-03-15 20:57:18 +01:00
{
2019-02-01 17:43:31 +01:00
var settings = new XmlReaderSettings ( )
2017-03-15 20:57:18 +01:00
{
2019-02-01 17:43:31 +01:00
ValidationType = ValidationType . None ,
CheckCharacters = false ,
IgnoreProcessingInstructions = true ,
IgnoreComments = true
} ;
2017-03-15 20:57:18 +01:00
2019-02-01 17:43:31 +01:00
using ( var reader = XmlReader . Create ( oReader , settings ) )
{
var result = ReleaseResult . Parse ( reader ) . FirstOrDefault ( ) ;
2017-10-20 18:16:56 +02:00
2019-02-01 17:43:31 +01:00
if ( result ! = null )
{
return result . ReleaseId ;
2017-03-15 20:57:18 +01:00
}
}
}
return null ;
}
2013-11-12 18:12:11 +01:00
/// <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>
2017-03-15 20:57:18 +01:00
private async Task < string > GetReleaseGroupFromReleaseId ( string releaseEntryId , CancellationToken cancellationToken )
2013-11-12 18:12:11 +01:00
{
2019-09-10 22:37:53 +02:00
var url = "/ws/2/release-group/?query=reid:" + releaseEntryId . ToString ( CultureInfo . InvariantCulture ) ;
2013-11-12 18:12:11 +01:00
2019-03-13 21:31:21 +01:00
using ( var response = await GetMusicBrainzResponse ( url , cancellationToken ) . ConfigureAwait ( false ) )
2019-02-01 17:43:31 +01:00
using ( var stream = response . Content )
using ( var oReader = new StreamReader ( stream , Encoding . UTF8 ) )
2016-06-15 22:14:04 +02:00
{
2019-02-01 17:43:31 +01:00
var settings = new XmlReaderSettings ( )
2016-10-27 21:03:23 +02:00
{
2019-02-01 17:43:31 +01:00
ValidationType = ValidationType . None ,
CheckCharacters = false ,
IgnoreProcessingInstructions = true ,
IgnoreComments = true
} ;
2016-10-27 23:05:25 +02:00
2019-02-01 17:43:31 +01:00
using ( var reader = XmlReader . Create ( oReader , settings ) )
{
reader . MoveToContent ( ) ;
reader . Read ( ) ;
2017-10-20 18:16:56 +02:00
2019-02-01 17:43:31 +01:00
// Loop through each element
while ( ! reader . EOF & & reader . ReadState = = ReadState . Interactive )
{
if ( reader . NodeType = = XmlNodeType . Element )
2016-10-27 23:05:25 +02:00
{
2019-02-01 17:43:31 +01:00
switch ( reader . Name )
2016-10-27 21:03:23 +02:00
{
2019-02-01 17:43:31 +01:00
case "release-group-list" :
2017-10-20 18:16:56 +02:00
{
2019-02-01 17:43:31 +01:00
if ( reader . IsEmptyElement )
{
reader . Read ( ) ;
continue ;
}
using ( var subReader = reader . ReadSubtree ( ) )
{
return GetFirstReleaseGroupId ( subReader ) ;
}
}
default :
{
reader . Skip ( ) ;
break ;
2017-10-20 18:16:56 +02:00
}
2016-10-27 21:03:23 +02:00
}
2019-02-01 17:43:31 +01:00
}
else
{
reader . Read ( ) ;
2016-10-27 23:05:25 +02:00
}
2016-10-27 21:03:23 +02:00
}
2019-09-10 22:37:53 +02:00
2019-02-01 17:43:31 +01:00
return null ;
2016-10-27 21:03:23 +02:00
}
2016-06-15 22:14:04 +02:00
}
2016-10-27 21:03:23 +02:00
}
2013-11-12 18:12:11 +01:00
2016-10-27 21:03:23 +02:00
private string GetFirstReleaseGroupId ( XmlReader reader )
{
reader . MoveToContent ( ) ;
2016-11-02 18:08:20 +01:00
reader . Read ( ) ;
2016-06-15 22:14:04 +02:00
2016-10-27 21:03:23 +02:00
// Loop through each element
2016-12-03 22:46:06 +01:00
while ( ! reader . EOF & & reader . ReadState = = ReadState . Interactive )
2016-06-15 22:14:04 +02:00
{
2016-10-27 23:05:25 +02:00
if ( reader . NodeType = = XmlNodeType . Element )
2016-06-15 22:14:04 +02:00
{
2016-10-27 23:05:25 +02:00
switch ( reader . Name )
{
case "release-group" :
{
return reader . GetAttribute ( "id" ) ;
}
default :
{
reader . Skip ( ) ;
break ;
}
}
2016-06-15 22:14:04 +02:00
}
2016-10-31 19:59:58 +01:00
else
{
reader . Read ( ) ;
}
2016-06-15 22:14:04 +02:00
}
2016-10-27 21:03:23 +02:00
2016-06-15 22:14:04 +02:00
return null ;
2013-11-12 18:12:11 +01:00
}
/// <summary>
2019-03-13 21:31:21 +01:00
/// Makes request to MusicBrainz server and awaits a response.
2019-03-15 20:29:04 +01:00
/// 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.
2013-11-12 18:12:11 +01:00
/// </summary>
2019-03-13 21:31:21 +01:00
internal async Task < HttpResponseInfo > GetMusicBrainzResponse ( string url , CancellationToken cancellationToken )
2013-11-12 18:12:11 +01:00
{
2014-10-11 22:38:13 +02:00
var options = new HttpRequestOptions
{
2019-09-10 22:37:53 +02:00
Url = _musicBrainzBaseUrl . TrimEnd ( '/' ) + url ,
2014-10-11 22:38:13 +02:00
CancellationToken = cancellationToken ,
2019-03-14 22:32:27 +01:00
// MusicBrainz request a contact email address is supplied, as comment, in user agent field:
// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent
2019-09-10 22:37:53 +02:00
UserAgent = string . Format (
CultureInfo . InvariantCulture ,
"{0} ( {1} )" ,
_appHost . ApplicationUserAgent ,
_appHost . ApplicationUserAgentAddress ) ,
2019-03-13 21:31:21 +01:00
BufferContent = false
2014-10-11 22:38:13 +02:00
} ;
2014-09-28 18:50:33 +02:00
2019-03-15 20:29:04 +01:00
HttpResponseInfo response ;
var attempts = 0 u ;
do
{
attempts + + ;
2020-02-22 07:04:52 +01:00
if ( _stopWatchMusicBrainz . ElapsedMilliseconds < _musicBrainzQueryIntervalMs )
2019-03-15 20:29:04 +01:00
{
// MusicBrainz is extremely adamant about limiting to one request per second
2020-02-22 07:04:52 +01:00
var delayMs = _musicBrainzQueryIntervalMs - _stopWatchMusicBrainz . ElapsedMilliseconds ;
2019-03-15 20:29:04 +01:00
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 ( ) ;
response = await _httpClient . SendAsync ( options , "GET" ) . ConfigureAwait ( false ) ;
2020-02-22 07:04:52 +01:00
// We retry a finite number of times, and only whilst MB is indicating 503 (throttling)
2019-03-15 20:29:04 +01:00
}
while ( attempts < MusicBrainzQueryAttempts & & response . StatusCode = = HttpStatusCode . ServiceUnavailable ) ;
// Log error if unable to query MB database due to throttling
2019-09-10 22:37:53 +02:00
if ( attempts = = MusicBrainzQueryAttempts & & response . StatusCode = = HttpStatusCode . ServiceUnavailable )
2019-03-15 20:29:04 +01:00
{
_logger . LogError ( "GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}" , attempts , options . Url ) ;
}
return response ;
2013-11-12 18:12:11 +01:00
}
2019-09-10 22:37:53 +02:00
/// <inheritdoc />
2014-02-20 05:53:15 +01:00
public Task < HttpResponseInfo > GetImageResponse ( string url , CancellationToken cancellationToken )
{
throw new NotImplementedException ( ) ;
}
2013-11-12 18:12:11 +01:00
}
2018-12-28 16:48:26 +01:00
}