musicbrainz.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. import axios from "axios";
  2. import CoreClass from "../core";
  3. import { MUSARE_VERSION } from "..";
  4. class RateLimitter {
  5. /**
  6. * Constructor
  7. * @param {number} timeBetween - The time between each allowed MusicBrainz request
  8. */
  9. constructor(timeBetween) {
  10. this.dateStarted = Date.now();
  11. this.timeBetween = timeBetween;
  12. }
  13. /**
  14. * Returns a promise that resolves whenever the ratelimit of a MusicBrainz request is done
  15. * @returns {Promise} - promise that gets resolved when the rate limit allows it
  16. */
  17. continue() {
  18. return new Promise(resolve => {
  19. if (Date.now() - this.dateStarted >= this.timeBetween) resolve();
  20. else setTimeout(resolve, this.dateStarted + this.timeBetween - Date.now());
  21. });
  22. }
  23. /**
  24. * Restart the rate limit timer
  25. */
  26. restart() {
  27. this.dateStarted = Date.now();
  28. }
  29. }
  30. let MusicBrainzModule;
  31. let DBModule;
  32. let CacheModule;
  33. class _MusicBrainzModule extends CoreClass {
  34. // eslint-disable-next-line require-jsdoc
  35. constructor() {
  36. super("musicbrainz", {
  37. concurrency: 1
  38. });
  39. MusicBrainzModule = this;
  40. }
  41. /**
  42. * Initialises the MusicBrainz module
  43. * @returns {Promise} - returns promise (reject, resolve)
  44. */
  45. async initialize() {
  46. DBModule = this.moduleManager.modules.db;
  47. CacheModule = this.moduleManager.modules.cache;
  48. this.genericApiRequestModel = this.GenericApiRequestModel = await DBModule.runJob("GET_MODEL", {
  49. modelName: "genericApiRequest"
  50. });
  51. this.rateLimiter = new RateLimitter(1100);
  52. this.requestTimeout = 5000;
  53. this.axios = axios.create();
  54. }
  55. /**
  56. * Perform MusicBrainz API call
  57. * @param {object} payload - object that contains the payload
  58. * @param {string} payload.url - request url
  59. * @param {object} payload.params - request parameters
  60. * @returns {Promise} - returns promise (reject, resolve)
  61. */
  62. async API_CALL(payload) {
  63. const { url, params } = payload;
  64. let genericApiRequest = await MusicBrainzModule.GenericApiRequestModel.findOne({
  65. url,
  66. params
  67. });
  68. if (genericApiRequest) {
  69. if (genericApiRequest._doc.responseData.error) throw new Error(genericApiRequest._doc.responseData.error);
  70. return genericApiRequest._doc.responseData;
  71. }
  72. await MusicBrainzModule.rateLimiter.continue();
  73. MusicBrainzModule.rateLimiter.restart();
  74. const responseData = await new Promise((resolve, reject) => {
  75. console.log(
  76. "INFO",
  77. `Making MusicBrainz API request to ${url} with ${new URLSearchParams(params).toString()}`
  78. );
  79. MusicBrainzModule.axios
  80. .get(url, {
  81. params,
  82. headers: {
  83. "User-Agent": `Musare/${MUSARE_VERSION} ( https://github.com/Musare/Musare )` // TODO set this in accordance to https://musicbrainz.org/doc/MusicBrainz_API/Rate_Limiting
  84. },
  85. timeout: MusicBrainzModule.requestTimeout
  86. })
  87. .then(({ data: responseData }) => {
  88. resolve(responseData);
  89. })
  90. .catch(err => {
  91. if (err.response.status === 404) {
  92. resolve(err.response.data);
  93. } else reject(err);
  94. });
  95. });
  96. if (responseData.error && responseData.error !== "Not Found") throw new Error(responseData.error);
  97. genericApiRequest = new MusicBrainzModule.GenericApiRequestModel({
  98. url,
  99. params,
  100. responseData,
  101. date: Date.now()
  102. });
  103. genericApiRequest.save();
  104. if (responseData.error) throw new Error(responseData.error);
  105. return responseData;
  106. }
  107. /**
  108. * Gets a list of recordings, releases and release groups for a given artist
  109. * @param {object} payload - object that contains the payload
  110. * @param {string} payload.artistId - MusicBrainz artist id
  111. * @returns {Promise} - returns promise (reject, resolve)
  112. */
  113. async GET_RECORDINGS_RELEASES_RELEASE_GROUPS(payload) {
  114. const { artistId } = payload;
  115. // TODO this job caches a response to prevent overloading the API, but doesn't do anything to prevent the same job for the same artistId from running more than once at the same time
  116. const existingResponse = await CacheModule.runJob(
  117. "HGET",
  118. {
  119. table: "musicbrainzRecordingsReleasesReleaseGroups",
  120. key: artistId
  121. },
  122. this
  123. );
  124. if (existingResponse) return existingResponse;
  125. const fetchDate = new Date();
  126. let maxReleases = 0;
  127. let releases = [];
  128. do {
  129. const offset = releases.length;
  130. // eslint-disable-next-line no-await-in-loop
  131. const response = await MusicBrainzModule.runJob(
  132. "GET_RECORDINGS_RELEASES_RELEASE_GROUPS_PAGE",
  133. { artistId, offset },
  134. this
  135. );
  136. maxReleases = response["release-count"];
  137. releases = [...releases, ...response.releases];
  138. if (response.releases.length === 0) break;
  139. } while (maxReleases >= releases.length);
  140. const response = {
  141. releases,
  142. fetchDate
  143. };
  144. await CacheModule.runJob(
  145. "HSET",
  146. {
  147. table: "musicbrainzRecordingsReleasesReleaseGroups",
  148. key: artistId,
  149. value: JSON.stringify(response)
  150. },
  151. this
  152. );
  153. return response;
  154. }
  155. /**
  156. * Gets a list of recordings, releases and release groups for a given artist, for a given page
  157. * @param {object} payload - object that contains the payload
  158. * @param {string} payload.artistId - MusicBrainz artist id
  159. * @param {string} payload.offset - offset by how much
  160. * @returns {Promise} - returns promise (reject, resolve)
  161. */
  162. async GET_RECORDINGS_RELEASES_RELEASE_GROUPS_PAGE(payload) {
  163. const { artistId, offset } = payload;
  164. const response = await MusicBrainzModule.runJob(
  165. "API_CALL",
  166. {
  167. url: `https://musicbrainz.org/ws/2/release`,
  168. params: {
  169. fmt: "json",
  170. artist: artistId,
  171. inc: "release-groups+recordings",
  172. limit: 100,
  173. offset
  174. }
  175. },
  176. this
  177. );
  178. return response;
  179. }
  180. }
  181. export default new _MusicBrainzModule();