using MediaBrowser.Common; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Providers; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; using MediaBrowser.Model.Serialization; namespace MediaBrowser.Providers.Music { public class MusicBrainzAlbumProvider : IRemoteMetadataProvider, IHasOrder { internal static MusicBrainzAlbumProvider Current; private readonly IHttpClient _httpClient; private readonly IApplicationHost _appHost; private readonly ILogger _logger; private readonly IJsonSerializer _json; public static string MusicBrainzBaseUrl = "https://www.musicbrainz.org"; public MusicBrainzAlbumProvider(IHttpClient httpClient, IApplicationHost appHost, ILogger logger, IJsonSerializer json) { _httpClient = httpClient; _appHost = appHost; _logger = logger; _json = json; Current = this; } public async Task> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken) { var releaseId = searchInfo.GetReleaseId(); string url = null; var isNameSearch = false; if (!string.IsNullOrEmpty(releaseId)) { url = string.Format("/ws/2/release/?query=reid:{0}", releaseId); } else { var artistMusicBrainzId = searchInfo.GetMusicBrainzArtistId(); if (!string.IsNullOrWhiteSpace(artistMusicBrainzId)) { url = string.Format("/ws/2/release/?query=\"{0}\" AND arid:{1}", WebUtility.UrlEncode(searchInfo.Name), artistMusicBrainzId); } else { isNameSearch = true; url = string.Format("/ws/2/release/?query=\"{0}\" AND artist:\"{1}\"", WebUtility.UrlEncode(searchInfo.Name), WebUtility.UrlEncode(searchInfo.GetAlbumArtist())); } } if (!string.IsNullOrWhiteSpace(url)) { var doc = await GetMusicBrainzResponse(url, isNameSearch, cancellationToken).ConfigureAwait(false); return GetResultsFromResponse(doc); } return new List(); } private IEnumerable GetResultsFromResponse(XmlDocument doc) { return ReleaseResult.Parse(doc).Select(i => { var result = new RemoteSearchResult { Name = i.Title, ProductionYear = i.Year }; if (!string.IsNullOrWhiteSpace(i.ReleaseId)) { result.SetProviderId(MetadataProviders.MusicBrainzAlbum, i.ReleaseId); } if (!string.IsNullOrWhiteSpace(i.ReleaseGroupId)) { result.SetProviderId(MetadataProviders.MusicBrainzReleaseGroup, i.ReleaseGroupId); } return result; }); } public async Task> GetMetadata(AlbumInfo id, CancellationToken cancellationToken) { var releaseId = id.GetReleaseId(); var releaseGroupId = id.GetReleaseGroupId(); var result = new MetadataResult { Item = new MusicAlbum() }; if (string.IsNullOrEmpty(releaseId)) { var artistMusicBrainzId = id.GetMusicBrainzArtistId(); var releaseResult = await GetReleaseResult(artistMusicBrainzId, id.GetAlbumArtist(), id.Name, cancellationToken).ConfigureAwait(false); if (releaseResult != null) { if (!string.IsNullOrEmpty(releaseResult.ReleaseId)) { releaseId = releaseResult.ReleaseId; result.HasMetadata = true; } if (!string.IsNullOrEmpty(releaseResult.ReleaseGroupId)) { releaseGroupId = releaseResult.ReleaseGroupId; result.HasMetadata = true; } result.Item.ProductionYear = releaseResult.Year; result.Item.Overview = releaseResult.Overview; } } // If we have a release Id but not a release group Id... if (!string.IsNullOrEmpty(releaseId) && string.IsNullOrEmpty(releaseGroupId)) { releaseGroupId = await GetReleaseGroupId(releaseId, cancellationToken).ConfigureAwait(false); result.HasMetadata = true; } if (!string.IsNullOrEmpty(releaseId) || !string.IsNullOrEmpty(releaseGroupId)) { result.HasMetadata = true; } if (result.HasMetadata) { if (!string.IsNullOrEmpty(releaseId)) { result.Item.SetProviderId(MetadataProviders.MusicBrainzAlbum, releaseId); } if (!string.IsNullOrEmpty(releaseGroupId)) { result.Item.SetProviderId(MetadataProviders.MusicBrainzReleaseGroup, releaseGroupId); } } return result; } public string Name { get { return "MusicBrainz"; } } private Task 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 GetReleaseResult(string albumName, string artistId, CancellationToken cancellationToken) { var url = string.Format("/ws/2/release/?query=\"{0}\" AND arid:{1}", WebUtility.UrlEncode(albumName), artistId); var doc = await GetMusicBrainzResponse(url, true, cancellationToken).ConfigureAwait(false); return ReleaseResult.Parse(doc, 1).FirstOrDefault(); } private async Task GetReleaseResultByArtistName(string albumName, string artistName, CancellationToken cancellationToken) { var url = string.Format("/ws/2/release/?query=\"{0}\" AND artist:\"{1}\"", WebUtility.UrlEncode(albumName), WebUtility.UrlEncode(artistName)); var doc = await GetMusicBrainzResponse(url, true, cancellationToken).ConfigureAwait(false); return ReleaseResult.Parse(doc, 1).FirstOrDefault(); } private class ReleaseResult { public string ReleaseId; public string ReleaseGroupId; public string Title; public string Overview; public int? Year; public static List Parse(XmlDocument doc, int? limit = null) { var docElem = doc.DocumentElement; var list = new List(); if (docElem == null) { return list; } var releaseList = docElem.FirstChild; if (releaseList == null) { return list; } var nodes = releaseList.ChildNodes; if (nodes != null) { foreach (var node in nodes.Cast()) { if (string.Equals(node.Name, "release", StringComparison.OrdinalIgnoreCase)) { var releaseId = node.Attributes["id"].Value; var releaseGroupId = GetReleaseGroupIdFromReleaseNode(node); list.Add(new ReleaseResult { ReleaseId = releaseId, ReleaseGroupId = releaseGroupId, Title = GetValueFromReleaseNode(node, "title"), Overview = GetValueFromReleaseNode(node, "annotation"), Year = GetYearFromReleaseNode(node, "date") }); if (limit.HasValue && list.Count >= limit.Value) { break; } } } } return list; } private static int? GetYearFromReleaseNode(XmlNode node, string name) { var subNodes = node.ChildNodes; if (subNodes != null) { foreach (var subNode in subNodes.Cast()) { if (string.Equals(subNode.Name, name, StringComparison.OrdinalIgnoreCase)) { DateTime date; if (DateTime.TryParse(subNode.InnerText, out date)) { return date.Year; } return null; } } } return null; } private static string GetValueFromReleaseNode(XmlNode node, string name) { var subNodes = node.ChildNodes; if (subNodes != null) { foreach (var subNode in subNodes.Cast()) { if (string.Equals(subNode.Name, name, StringComparison.OrdinalIgnoreCase)) { return subNode.InnerText; } } } return null; } private static string GetReleaseGroupIdFromReleaseNode(XmlNode node) { var subNodes = node.ChildNodes; if (subNodes != null) { foreach (var subNode in subNodes.Cast()) { if (string.Equals(subNode.Name, "release-group", StringComparison.OrdinalIgnoreCase)) { return subNode.Attributes["id"].Value; } } } return null; } } /// /// Gets the release group id internal. /// /// The release entry id. /// The cancellation token. /// Task{System.String}. private async Task GetReleaseGroupId(string releaseEntryId, CancellationToken cancellationToken) { var url = string.Format("/ws/2/release-group/?query=reid:{0}", releaseEntryId); var doc = await GetMusicBrainzResponse(url, false, cancellationToken).ConfigureAwait(false); var docElem = doc.DocumentElement; if (docElem == null) { return null; } var releaseList = docElem.FirstChild; if (releaseList == null) { return null; } var nodes = releaseList.ChildNodes; if (nodes != null) { foreach (var node in nodes.Cast()) { if (string.Equals(node.Name, "release-group", StringComparison.OrdinalIgnoreCase)) { return node.Attributes["id"].Value; } } } return null; } /// /// The _music brainz resource pool /// private readonly SemaphoreSlim _musicBrainzResourcePool = new SemaphoreSlim(1, 1); private long _lastMbzUrlQueryTicks = 0; private List _mbzUrls = null; private MbzUrl _chosenUrl; private async Task GetMbzUrl() { if (_chosenUrl == null || _mbzUrls == null || (DateTime.UtcNow.Ticks - _lastMbzUrlQueryTicks) > TimeSpan.FromHours(12).Ticks) { var urls = await RefreshMzbUrls().ConfigureAwait(false); if (urls.Count > 1) { _chosenUrl = urls[new Random().Next(0, urls.Count)]; } else { _chosenUrl = urls[0]; } } return _chosenUrl; } private async Task> RefreshMzbUrls() { List list; try { var options = new HttpRequestOptions { Url = "https://mb3admin.com/admin/service/standards/musicBrainzUrls", UserAgent = _appHost.Name + "/" + _appHost.ApplicationVersion }; using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) { list = _json.DeserializeFromStream>(stream); } _lastMbzUrlQueryTicks = DateTime.UtcNow.Ticks; } catch (Exception ex) { _logger.ErrorException("Error getting music brainz info", ex); list = new List { new MbzUrl { url = MusicBrainzBaseUrl, throttleMs = 1000 } }; } _mbzUrls = list.ToList(); return list; } /// /// Gets the music brainz response. /// /// The URL. /// if set to true [is search]. /// The cancellation token. /// Task{XmlDocument}. internal async Task GetMusicBrainzResponse(string url, bool isSearch, CancellationToken cancellationToken) { var urlInfo = await GetMbzUrl().ConfigureAwait(false); if (urlInfo.throttleMs > 0) { // MusicBrainz is extremely adamant about limiting to one request per second await Task.Delay(urlInfo.throttleMs, cancellationToken).ConfigureAwait(false); } url = urlInfo.url.TrimEnd('/') + url; var doc = new XmlDocument(); var options = new HttpRequestOptions { Url = url, CancellationToken = cancellationToken, UserAgent = _appHost.Name + "/" + _appHost.ApplicationVersion, ResourcePool = _musicBrainzResourcePool }; using (var xml = await _httpClient.Get(options).ConfigureAwait(false)) { using (var oReader = new StreamReader(xml, Encoding.UTF8)) { doc.Load(oReader); } } return doc; } public int Order { get { return 0; } } public Task GetImageResponse(string url, CancellationToken cancellationToken) { throw new NotImplementedException(); } internal class MbzUrl { public string url { get; set; } public int throttleMs { get; set; } } } }