123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- 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();
|