MusicBrainzAlbumProvider.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. using MediaBrowser.Common;
  2. using MediaBrowser.Common.Net;
  3. using MediaBrowser.Controller.Entities.Audio;
  4. using MediaBrowser.Controller.Providers;
  5. using MediaBrowser.Model.Entities;
  6. using MediaBrowser.Model.Logging;
  7. using MediaBrowser.Model.Providers;
  8. using System;
  9. using System.Collections.Generic;
  10. using System.IO;
  11. using System.Linq;
  12. using System.Net;
  13. using System.Text;
  14. using System.Threading;
  15. using System.Threading.Tasks;
  16. using System.Xml;
  17. using MediaBrowser.Model.Serialization;
  18. namespace MediaBrowser.Providers.Music
  19. {
  20. public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder
  21. {
  22. internal static MusicBrainzAlbumProvider Current;
  23. private readonly IHttpClient _httpClient;
  24. private readonly IApplicationHost _appHost;
  25. private readonly ILogger _logger;
  26. private readonly IJsonSerializer _json;
  27. public static string MusicBrainzBaseUrl = "https://www.musicbrainz.org";
  28. public MusicBrainzAlbumProvider(IHttpClient httpClient, IApplicationHost appHost, ILogger logger, IJsonSerializer json)
  29. {
  30. _httpClient = httpClient;
  31. _appHost = appHost;
  32. _logger = logger;
  33. _json = json;
  34. Current = this;
  35. }
  36. public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken)
  37. {
  38. var releaseId = searchInfo.GetReleaseId();
  39. string url = null;
  40. var isNameSearch = false;
  41. if (!string.IsNullOrEmpty(releaseId))
  42. {
  43. url = string.Format("/ws/2/release/?query=reid:{0}", releaseId);
  44. }
  45. else
  46. {
  47. var artistMusicBrainzId = searchInfo.GetMusicBrainzArtistId();
  48. if (!string.IsNullOrWhiteSpace(artistMusicBrainzId))
  49. {
  50. url = string.Format("/ws/2/release/?query=\"{0}\" AND arid:{1}",
  51. WebUtility.UrlEncode(searchInfo.Name),
  52. artistMusicBrainzId);
  53. }
  54. else
  55. {
  56. isNameSearch = true;
  57. url = string.Format("/ws/2/release/?query=\"{0}\" AND artist:\"{1}\"",
  58. WebUtility.UrlEncode(searchInfo.Name),
  59. WebUtility.UrlEncode(searchInfo.GetAlbumArtist()));
  60. }
  61. }
  62. if (!string.IsNullOrWhiteSpace(url))
  63. {
  64. var doc = await GetMusicBrainzResponse(url, isNameSearch, cancellationToken).ConfigureAwait(false);
  65. return GetResultsFromResponse(doc);
  66. }
  67. return new List<RemoteSearchResult>();
  68. }
  69. private IEnumerable<RemoteSearchResult> GetResultsFromResponse(XmlDocument doc)
  70. {
  71. return ReleaseResult.Parse(doc).Select(i =>
  72. {
  73. var result = new RemoteSearchResult
  74. {
  75. Name = i.Title
  76. };
  77. if (!string.IsNullOrWhiteSpace(i.ReleaseId))
  78. {
  79. result.SetProviderId(MetadataProviders.MusicBrainzAlbum, i.ReleaseId);
  80. }
  81. if (!string.IsNullOrWhiteSpace(i.ReleaseGroupId))
  82. {
  83. result.SetProviderId(MetadataProviders.MusicBrainzAlbum, i.ReleaseGroupId);
  84. }
  85. return result;
  86. });
  87. }
  88. public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo id, CancellationToken cancellationToken)
  89. {
  90. var releaseId = id.GetReleaseId();
  91. var releaseGroupId = id.GetReleaseGroupId();
  92. var result = new MetadataResult<MusicAlbum>
  93. {
  94. Item = new MusicAlbum()
  95. };
  96. if (string.IsNullOrEmpty(releaseId))
  97. {
  98. var artistMusicBrainzId = id.GetMusicBrainzArtistId();
  99. var releaseResult = await GetReleaseResult(artistMusicBrainzId, id.GetAlbumArtist(), id.Name, cancellationToken).ConfigureAwait(false);
  100. if (!string.IsNullOrEmpty(releaseResult.ReleaseId))
  101. {
  102. releaseId = releaseResult.ReleaseId;
  103. result.HasMetadata = true;
  104. }
  105. if (!string.IsNullOrEmpty(releaseResult.ReleaseGroupId))
  106. {
  107. releaseGroupId = releaseResult.ReleaseGroupId;
  108. result.HasMetadata = true;
  109. }
  110. }
  111. // If we have a release Id but not a release group Id...
  112. if (!string.IsNullOrEmpty(releaseId) && string.IsNullOrEmpty(releaseGroupId))
  113. {
  114. releaseGroupId = await GetReleaseGroupId(releaseId, cancellationToken).ConfigureAwait(false);
  115. result.HasMetadata = true;
  116. }
  117. if (!string.IsNullOrEmpty(releaseId) || !string.IsNullOrEmpty(releaseGroupId))
  118. {
  119. result.HasMetadata = true;
  120. }
  121. if (result.HasMetadata)
  122. {
  123. if (!string.IsNullOrEmpty(releaseId))
  124. {
  125. result.Item.SetProviderId(MetadataProviders.MusicBrainzAlbum, releaseId);
  126. }
  127. if (!string.IsNullOrEmpty(releaseGroupId))
  128. {
  129. result.Item.SetProviderId(MetadataProviders.MusicBrainzReleaseGroup, releaseGroupId);
  130. }
  131. }
  132. return result;
  133. }
  134. public string Name
  135. {
  136. get { return "MusicBrainz"; }
  137. }
  138. private Task<ReleaseResult> GetReleaseResult(string artistMusicBrainId, string artistName, string albumName, CancellationToken cancellationToken)
  139. {
  140. if (!string.IsNullOrEmpty(artistMusicBrainId))
  141. {
  142. return GetReleaseResult(albumName, artistMusicBrainId, cancellationToken);
  143. }
  144. if (string.IsNullOrWhiteSpace(artistName))
  145. {
  146. return Task.FromResult(new ReleaseResult());
  147. }
  148. return GetReleaseResultByArtistName(albumName, artistName, cancellationToken);
  149. }
  150. private async Task<ReleaseResult> GetReleaseResult(string albumName, string artistId, CancellationToken cancellationToken)
  151. {
  152. var url = string.Format("/ws/2/release/?query=\"{0}\" AND arid:{1}",
  153. WebUtility.UrlEncode(albumName),
  154. artistId);
  155. var doc = await GetMusicBrainzResponse(url, true, cancellationToken).ConfigureAwait(false);
  156. return ReleaseResult.Parse(doc, 1).FirstOrDefault();
  157. }
  158. private async Task<ReleaseResult> GetReleaseResultByArtistName(string albumName, string artistName, CancellationToken cancellationToken)
  159. {
  160. var url = string.Format("/ws/2/release/?query=\"{0}\" AND artist:\"{1}\"",
  161. WebUtility.UrlEncode(albumName),
  162. WebUtility.UrlEncode(artistName));
  163. var doc = await GetMusicBrainzResponse(url, true, cancellationToken).ConfigureAwait(false);
  164. return ReleaseResult.Parse(doc, 1).FirstOrDefault();
  165. }
  166. private class ReleaseResult
  167. {
  168. public string ReleaseId;
  169. public string ReleaseGroupId;
  170. public string Title;
  171. public static List<ReleaseResult> Parse(XmlDocument doc, int? limit = null)
  172. {
  173. var docElem = doc.DocumentElement;
  174. var list = new List<ReleaseResult>();
  175. if (docElem == null)
  176. {
  177. return list;
  178. }
  179. var releaseList = docElem.FirstChild;
  180. if (releaseList == null)
  181. {
  182. return list;
  183. }
  184. var nodes = releaseList.ChildNodes;
  185. if (nodes != null)
  186. {
  187. foreach (var node in nodes.Cast<XmlNode>())
  188. {
  189. if (string.Equals(node.Name, "release", StringComparison.OrdinalIgnoreCase))
  190. {
  191. var releaseId = node.Attributes["id"].Value;
  192. var releaseGroupId = GetReleaseGroupIdFromReleaseNode(node);
  193. list.Add(new ReleaseResult
  194. {
  195. ReleaseId = releaseId,
  196. ReleaseGroupId = releaseGroupId,
  197. Title = GetTitleFromReleaseNode(node)
  198. });
  199. if (limit.HasValue && list.Count >= limit.Value)
  200. {
  201. break;
  202. }
  203. }
  204. }
  205. }
  206. return list;
  207. }
  208. private static string GetTitleFromReleaseNode(XmlNode node)
  209. {
  210. var subNodes = node.ChildNodes;
  211. if (subNodes != null)
  212. {
  213. foreach (var subNode in subNodes.Cast<XmlNode>())
  214. {
  215. if (string.Equals(subNode.Name, "title", StringComparison.OrdinalIgnoreCase))
  216. {
  217. return subNode.InnerText;
  218. }
  219. }
  220. }
  221. return null;
  222. }
  223. private static string GetReleaseGroupIdFromReleaseNode(XmlNode node)
  224. {
  225. var subNodes = node.ChildNodes;
  226. if (subNodes != null)
  227. {
  228. foreach (var subNode in subNodes.Cast<XmlNode>())
  229. {
  230. if (string.Equals(subNode.Name, "release-group", StringComparison.OrdinalIgnoreCase))
  231. {
  232. return subNode.Attributes["id"].Value;
  233. }
  234. }
  235. }
  236. return null;
  237. }
  238. }
  239. /// <summary>
  240. /// Gets the release group id internal.
  241. /// </summary>
  242. /// <param name="releaseEntryId">The release entry id.</param>
  243. /// <param name="cancellationToken">The cancellation token.</param>
  244. /// <returns>Task{System.String}.</returns>
  245. private async Task<string> GetReleaseGroupId(string releaseEntryId, CancellationToken cancellationToken)
  246. {
  247. var url = string.Format("/ws/2/release-group/?query=reid:{0}", releaseEntryId);
  248. var doc = await GetMusicBrainzResponse(url, false, cancellationToken).ConfigureAwait(false);
  249. var docElem = doc.DocumentElement;
  250. if (docElem == null)
  251. {
  252. return null;
  253. }
  254. var releaseList = docElem.FirstChild;
  255. if (releaseList == null)
  256. {
  257. return null;
  258. }
  259. var nodes = releaseList.ChildNodes;
  260. if (nodes != null)
  261. {
  262. foreach (var node in nodes.Cast<XmlNode>())
  263. {
  264. if (string.Equals(node.Name, "release-group", StringComparison.OrdinalIgnoreCase))
  265. {
  266. return node.Attributes["id"].Value;
  267. }
  268. }
  269. }
  270. return null;
  271. }
  272. /// <summary>
  273. /// The _music brainz resource pool
  274. /// </summary>
  275. private readonly SemaphoreSlim _musicBrainzResourcePool = new SemaphoreSlim(1, 1);
  276. private long _lastMbzUrlQueryTicks = 0;
  277. private List<MbzUrl> _mbzUrls = null;
  278. private MbzUrl _chosenUrl;
  279. private async Task<MbzUrl> GetMbzUrl()
  280. {
  281. if (_chosenUrl == null || _mbzUrls == null || (DateTime.UtcNow.Ticks - _lastMbzUrlQueryTicks) > TimeSpan.FromHours(12).Ticks)
  282. {
  283. var urls = await RefreshMzbUrls().ConfigureAwait(false);
  284. if (urls.Count > 1)
  285. {
  286. _chosenUrl = urls[new Random().Next(0, urls.Count)];
  287. }
  288. else
  289. {
  290. _chosenUrl = urls[0];
  291. }
  292. }
  293. return _chosenUrl;
  294. }
  295. private async Task<List<MbzUrl>> RefreshMzbUrls()
  296. {
  297. List<MbzUrl> list;
  298. try
  299. {
  300. var options = new HttpRequestOptions
  301. {
  302. Url = "https://mb3admin.com/admin/service/standards/musicBrainzUrls",
  303. UserAgent = _appHost.Name + "/" + _appHost.ApplicationVersion
  304. };
  305. using (var stream = await _httpClient.Get(options).ConfigureAwait(false))
  306. {
  307. list = _json.DeserializeFromStream<List<MbzUrl>>(stream);
  308. }
  309. _lastMbzUrlQueryTicks = DateTime.UtcNow.Ticks;
  310. }
  311. catch (Exception ex)
  312. {
  313. _logger.ErrorException("Error getting music brainz info", ex);
  314. list = new List<MbzUrl>
  315. {
  316. new MbzUrl
  317. {
  318. url = MusicBrainzBaseUrl,
  319. throttleMs = 1000
  320. }
  321. };
  322. }
  323. _mbzUrls = list.ToList();
  324. return list;
  325. }
  326. /// <summary>
  327. /// Gets the music brainz response.
  328. /// </summary>
  329. /// <param name="url">The URL.</param>
  330. /// <param name="isSearch">if set to <c>true</c> [is search].</param>
  331. /// <param name="cancellationToken">The cancellation token.</param>
  332. /// <returns>Task{XmlDocument}.</returns>
  333. internal async Task<XmlDocument> GetMusicBrainzResponse(string url, bool isSearch, CancellationToken cancellationToken)
  334. {
  335. var urlInfo = await GetMbzUrl().ConfigureAwait(false);
  336. if (urlInfo.throttleMs > 0)
  337. {
  338. // MusicBrainz is extremely adamant about limiting to one request per second
  339. await Task.Delay(urlInfo.throttleMs, cancellationToken).ConfigureAwait(false);
  340. }
  341. url = urlInfo.url.TrimEnd('/') + url;
  342. var doc = new XmlDocument();
  343. var options = new HttpRequestOptions
  344. {
  345. Url = url,
  346. CancellationToken = cancellationToken,
  347. UserAgent = _appHost.Name + "/" + _appHost.ApplicationVersion,
  348. ResourcePool = _musicBrainzResourcePool
  349. };
  350. using (var xml = await _httpClient.Get(options).ConfigureAwait(false))
  351. {
  352. using (var oReader = new StreamReader(xml, Encoding.UTF8))
  353. {
  354. doc.Load(oReader);
  355. }
  356. }
  357. return doc;
  358. }
  359. public int Order
  360. {
  361. get { return 0; }
  362. }
  363. public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
  364. {
  365. throw new NotImplementedException();
  366. }
  367. internal class MbzUrl
  368. {
  369. public string url { get; set; }
  370. public int throttleMs { get; set; }
  371. }
  372. }
  373. }