import axios from "axios"; import CoreClass from "../core"; import { MUSARE_VERSION } from ".."; class RateLimitter { /** * Constructor * @param {number} timeBetween - The time between each allowed MusicBrainz request */ constructor(timeBetween) { this.dateStarted = Date.now(); this.timeBetween = timeBetween; } /** * Returns a promise that resolves whenever the ratelimit of a MusicBrainz request is done * @returns {Promise} - promise that gets resolved when the rate limit allows it */ continue() { return new Promise(resolve => { if (Date.now() - this.dateStarted >= this.timeBetween) resolve(); else setTimeout(resolve, this.dateStarted + this.timeBetween - Date.now()); }); } /** * Restart the rate limit timer */ restart() { this.dateStarted = Date.now(); } } let MusicBrainzModule; let DBModule; let CacheModule; class _MusicBrainzModule extends CoreClass { // eslint-disable-next-line require-jsdoc constructor() { super("musicbrainz", { concurrency: 1 }); MusicBrainzModule = this; } /** * Initialises the MusicBrainz module * @returns {Promise} - returns promise (reject, resolve) */ async initialize() { DBModule = this.moduleManager.modules.db; CacheModule = this.moduleManager.modules.cache; this.genericApiRequestModel = this.GenericApiRequestModel = await DBModule.runJob("GET_MODEL", { modelName: "genericApiRequest" }); this.rateLimiter = new RateLimitter(1100); this.requestTimeout = 5000; this.axios = axios.create(); } /** * Perform MusicBrainz API call * @param {object} payload - object that contains the payload * @param {string} payload.url - request url * @param {object} payload.params - request parameters * @returns {Promise} - returns promise (reject, resolve) */ async API_CALL(payload) { const { url, params } = payload; let genericApiRequest = await MusicBrainzModule.GenericApiRequestModel.findOne({ url, params }); if (genericApiRequest) { if (genericApiRequest._doc.responseData.error) throw new Error(genericApiRequest._doc.responseData.error); return genericApiRequest._doc.responseData; } await MusicBrainzModule.rateLimiter.continue(); MusicBrainzModule.rateLimiter.restart(); const responseData = await new Promise((resolve, reject) => { console.log( "INFO", `Making MusicBrainz API request to ${url} with ${new URLSearchParams(params).toString()}` ); MusicBrainzModule.axios .get(url, { params, headers: { "User-Agent": `Musare/${MUSARE_VERSION} ( https://github.com/Musare/Musare )` // TODO set this in accordance to https://musicbrainz.org/doc/MusicBrainz_API/Rate_Limiting }, timeout: MusicBrainzModule.requestTimeout }) .then(({ data: responseData }) => { resolve(responseData); }) .catch(err => { if (err.response.status === 404) { resolve(err.response.data); } else reject(err); }); }); if (responseData.error && responseData.error !== "Not Found") throw new Error(responseData.error); genericApiRequest = new MusicBrainzModule.GenericApiRequestModel({ url, params, responseData, date: Date.now() }); genericApiRequest.save(); if (responseData.error) throw new Error(responseData.error); return responseData; } /** * Gets a list of recordings, releases and release groups for a given artist * @param {object} payload - object that contains the payload * @param {string} payload.artistId - MusicBrainz artist id * @returns {Promise} - returns promise (reject, resolve) */ async GET_RECORDINGS_RELEASES_RELEASE_GROUPS(payload) { const { artistId } = payload; // 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 const existingResponse = await CacheModule.runJob( "HGET", { table: "musicbrainzRecordingsReleasesReleaseGroups", key: artistId }, this ); if (existingResponse) return existingResponse; const fetchDate = new Date(); let maxReleases = 0; let releases = []; do { const offset = releases.length; // eslint-disable-next-line no-await-in-loop const response = await MusicBrainzModule.runJob( "GET_RECORDINGS_RELEASES_RELEASE_GROUPS_PAGE", { artistId, offset }, this ); maxReleases = response["release-count"]; releases = [...releases, ...response.releases]; if (response.releases.length === 0) break; } while (maxReleases >= releases.length); const response = { releases, fetchDate }; await CacheModule.runJob( "HSET", { table: "musicbrainzRecordingsReleasesReleaseGroups", key: artistId, value: JSON.stringify(response) }, this ); return response; } /** * Gets a list of recordings, releases and release groups for a given artist, for a given page * @param {object} payload - object that contains the payload * @param {string} payload.artistId - MusicBrainz artist id * @param {string} payload.offset - offset by how much * @returns {Promise} - returns promise (reject, resolve) */ async GET_RECORDINGS_RELEASES_RELEASE_GROUPS_PAGE(payload) { const { artistId, offset } = payload; const response = await MusicBrainzModule.runJob( "API_CALL", { url: `https://musicbrainz.org/ws/2/release`, params: { fmt: "json", artist: artistId, inc: "release-groups+recordings", limit: 100, offset } }, this ); return response; } } export default new _MusicBrainzModule();