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