wikidata.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import axios from "axios";
  2. import CoreClass from "../core";
  3. class RateLimitter {
  4. /**
  5. * Constructor
  6. *
  7. * @param {number} timeBetween - The time between each allowed WikiData 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 WikiData request is done
  15. *
  16. * @returns {Promise} - promise that gets resolved when the rate limit allows it
  17. */
  18. continue() {
  19. return new Promise(resolve => {
  20. if (Date.now() - this.dateStarted >= this.timeBetween) resolve();
  21. else setTimeout(resolve, this.dateStarted + this.timeBetween - Date.now());
  22. });
  23. }
  24. /**
  25. * Restart the rate limit timer
  26. */
  27. restart() {
  28. this.dateStarted = Date.now();
  29. }
  30. }
  31. let WikiDataModule;
  32. let DBModule;
  33. class _WikiDataModule extends CoreClass {
  34. // eslint-disable-next-line require-jsdoc
  35. constructor() {
  36. super("wikidata", {
  37. concurrency: 10
  38. // priorities: {
  39. // GET_PLAYLIST: 11
  40. // }
  41. });
  42. WikiDataModule = this;
  43. }
  44. /**
  45. * Initialises the activities module
  46. *
  47. * @returns {Promise} - returns promise (reject, resolve)
  48. */
  49. async initialize() {
  50. DBModule = this.moduleManager.modules.db;
  51. this.genericApiRequestModel = this.GenericApiRequestModel = await DBModule.runJob("GET_MODEL", {
  52. modelName: "genericApiRequest"
  53. });
  54. // this.youtubeVideoModel = this.YoutubeVideoModel = await DBModule.runJob("GET_MODEL", {
  55. // modelName: "youtubeVideo"
  56. // });
  57. // return new Promise(resolve => {
  58. // CacheModule.runJob("SUB", {
  59. // channel: "youtube.removeYoutubeApiRequest",
  60. // cb: requestId => {
  61. // WSModule.runJob("EMIT_TO_ROOM", {
  62. // room: `view-api-request.${requestId}`,
  63. // args: ["event:youtubeApiRequest.removed"]
  64. // });
  65. // WSModule.runJob("EMIT_TO_ROOM", {
  66. // room: "admin.youtube",
  67. // args: ["event:admin.youtubeApiRequest.removed", { data: { requestId } }]
  68. // });
  69. // }
  70. // });
  71. // CacheModule.runJob("SUB", {
  72. // channel: "youtube.removeVideos",
  73. // cb: videoIds => {
  74. // const videos = Array.isArray(videoIds) ? videoIds : [videoIds];
  75. // videos.forEach(videoId => {
  76. // WSModule.runJob("EMIT_TO_ROOM", {
  77. // room: `view-media.youtube:${videoId}`,
  78. // args: ["event:youtubeVideo.removed"]
  79. // });
  80. // WSModule.runJob("EMIT_TO_ROOM", {
  81. // room: "admin.youtubeVideos",
  82. // args: ["event:admin.youtubeVideo.removed", { data: { videoId } }]
  83. // });
  84. // WSModule.runJob("EMIT_TO_ROOMS", {
  85. // rooms: ["import-album", "edit-songs"],
  86. // args: ["event:admin.youtubeVideo.removed", { videoId }]
  87. // });
  88. // });
  89. // }
  90. // });
  91. this.rateLimiter = new RateLimitter(1100);
  92. // this.requestTimeout = config.get("apis.youtube.requestTimeout");
  93. this.requestTimeout = 5000;
  94. this.axios = axios.create();
  95. // this.axios.defaults.raxConfig = {
  96. // instance: this.axios,
  97. // retry: config.get("apis.youtube.retryAmount"),
  98. // noResponseRetries: config.get("apis.youtube.retryAmount")
  99. // };
  100. // rax.attach(this.axios);
  101. // this.youtubeApiRequestModel
  102. // .find(
  103. // { date: { $gte: new Date() - 2 * 24 * 60 * 60 * 1000 } },
  104. // { date: true, quotaCost: true, _id: false }
  105. // )
  106. // .sort({ date: 1 })
  107. // .exec((err, youtubeApiRequests) => {
  108. // if (err) console.log("Couldn't load YouTube API requests.");
  109. // else {
  110. // this.apiCalls = youtubeApiRequests;
  111. // resolve();
  112. // }
  113. // });
  114. // resolve();
  115. // });
  116. }
  117. /**
  118. * Get WikiData data from entity url
  119. *
  120. * @param {object} payload - object that contains the payload
  121. * @param {object} payload.entityUrl - entity url
  122. * @returns {Promise} - returns promise (reject, resolve)
  123. */
  124. async API_GET_DATA_FROM_ENTITY_URL(payload) {
  125. const { entityUrl } = payload;
  126. const sparqlQuery = `PREFIX entity_url: <${entityUrl}>
  127. SELECT DISTINCT ?YouTube_video_ID ?SoundCloud_track_ID WHERE {
  128. OPTIONAL { entity_url: wdt:P1651 ?YouTube_video_ID. }
  129. OPTIONAL { entity_url: wdt:P3040 ?SoundCloud_track_ID. }
  130. }`
  131. .replaceAll("\n", "")
  132. .replaceAll("\t", "");
  133. return WikiDataModule.runJob(
  134. "API_CALL",
  135. {
  136. url: "https://query.wikidata.org/sparql",
  137. params: {
  138. query: sparqlQuery
  139. }
  140. },
  141. this
  142. );
  143. }
  144. /**
  145. * Get WikiData data from work id
  146. *
  147. * @param {object} payload - object that contains the payload
  148. * @param {object} payload.workId - work id
  149. * @returns {Promise} - returns promise (reject, resolve)
  150. */
  151. async API_GET_DATA_FROM_MUSICBRAINZ_WORK(payload) {
  152. const { workId } = payload;
  153. const sparqlQuery =
  154. `SELECT DISTINCT ?item ?itemLabel ?YouTube_video_ID ?SoundCloud_ID ?Music_video_entity_URL WHERE {
  155. SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE]". }
  156. {
  157. SELECT DISTINCT ?item WHERE {
  158. ?item p:P435 ?statement0.
  159. ?statement0 ps:P435 "${workId}".
  160. }
  161. LIMIT 100
  162. }
  163. OPTIONAL { ?item wdt:P1651 ?YouTube_video_ID. }
  164. OPTIONAL { ?item wdt:P3040 ?SoundCloud_ID. }
  165. OPTIONAL { ?item wdt:P6718 ?Music_video_entity_URL. }
  166. }`
  167. .replaceAll("\n", "")
  168. .replaceAll("\t", "");
  169. return WikiDataModule.runJob(
  170. "API_CALL",
  171. {
  172. url: "https://query.wikidata.org/sparql",
  173. params: {
  174. query: sparqlQuery
  175. }
  176. },
  177. this
  178. );
  179. }
  180. /**
  181. * Get WikiData data from release group id
  182. *
  183. * @param {object} payload - object that contains the payload
  184. * @param {object} payload.releaseGroupId - release group id
  185. * @returns {Promise} - returns promise (reject, resolve)
  186. */
  187. async API_GET_DATA_FROM_MUSICBRAINZ_RELEASE_GROUP(payload) {
  188. const { releaseGroupId } = payload;
  189. const sparqlQuery =
  190. `SELECT DISTINCT ?item ?itemLabel ?YouTube_video_ID ?SoundCloud_ID ?Music_video_entity_URL WHERE {
  191. SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE]". }
  192. {
  193. SELECT DISTINCT ?item WHERE {
  194. ?item p:P436 ?statement0.
  195. ?statement0 ps:P436 "${releaseGroupId}".
  196. }
  197. LIMIT 100
  198. }
  199. OPTIONAL { ?item wdt:P1651 ?YouTube_video_ID. }
  200. OPTIONAL { ?item wdt:P3040 ?SoundCloud_ID. }
  201. OPTIONAL { ?item wdt:P6718 ?Music_video_entity_URL. }
  202. }`
  203. .replaceAll("\n", "")
  204. .replaceAll("\t", "");
  205. return WikiDataModule.runJob(
  206. "API_CALL",
  207. {
  208. url: "https://query.wikidata.org/sparql",
  209. params: {
  210. query: sparqlQuery
  211. }
  212. },
  213. this
  214. );
  215. }
  216. /**
  217. * Get WikiData data from Spotify album id
  218. *
  219. * @param {object} payload - object that contains the payload
  220. * @param {object} payload.spotifyAlbumId - Spotify album id
  221. * @returns {Promise} - returns promise (reject, resolve)
  222. */
  223. async API_GET_DATA_FROM_SPOTIFY_ALBUM(payload) {
  224. const { spotifyAlbumId } = payload;
  225. if (!spotifyAlbumId) throw new Error("Invalid Spotify album ID provided.");
  226. const sparqlQuery = `SELECT DISTINCT ?item ?itemLabel ?YouTube_playlist_ID ?SoundCloud_ID WHERE {
  227. SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE]". }
  228. {
  229. SELECT DISTINCT ?item WHERE {
  230. ?item p:P2205 ?statement0.
  231. ?statement0 ps:P2205 "${spotifyAlbumId}".
  232. }
  233. LIMIT 100
  234. }
  235. OPTIONAL { ?item wdt:P4300 ?YouTube_playlist_ID. }
  236. OPTIONAL { ?item wdt:P3040 ?SoundCloud_ID. }
  237. }`
  238. .replaceAll("\n", "")
  239. .replaceAll("\t", "");
  240. return WikiDataModule.runJob(
  241. "API_CALL",
  242. {
  243. url: "https://query.wikidata.org/sparql",
  244. params: {
  245. query: sparqlQuery
  246. }
  247. },
  248. this
  249. );
  250. }
  251. /**
  252. * Get WikiData data from Spotify artist id
  253. *
  254. * @param {object} payload - object that contains the payload
  255. * @param {object} payload.spotifyArtistId - Spotify artist id
  256. * @returns {Promise} - returns promise (reject, resolve)
  257. */
  258. async API_GET_DATA_FROM_SPOTIFY_ARTIST(payload) {
  259. const { spotifyArtistId } = payload;
  260. if (!spotifyArtistId) throw new Error("Invalid Spotify artist ID provided.");
  261. const sparqlQuery =
  262. `SELECT DISTINCT ?item ?itemLabel ?YouTube_channel_ID ?SoundCloud_ID ?MusicBrainz_artist_ID WHERE {
  263. SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE]". }
  264. {
  265. SELECT DISTINCT ?item WHERE {
  266. ?item p:P1902 ?statement0.
  267. ?statement0 ps:P1902 "${spotifyArtistId}".
  268. }
  269. LIMIT 100
  270. }
  271. OPTIONAL { ?item wdt:P2397 ?YouTube_channel_ID. }
  272. OPTIONAL { ?item wdt:P3040 ?SoundCloud_ID. }
  273. OPTIONAL { ?item wdt:P434 ?MusicBrainz_artist_ID. }
  274. }`
  275. .replaceAll("\n", "")
  276. .replaceAll("\t", "");
  277. return WikiDataModule.runJob(
  278. "API_CALL",
  279. {
  280. url: "https://query.wikidata.org/sparql",
  281. params: {
  282. query: sparqlQuery
  283. }
  284. },
  285. this
  286. );
  287. }
  288. /**
  289. * Perform WikiData API call
  290. *
  291. * @param {object} payload - object that contains the payload
  292. * @param {object} payload.url - request url
  293. * @param {object} payload.params - request parameters
  294. * @param {object} payload.quotaCost - request quotaCost
  295. * @returns {Promise} - returns promise (reject, resolve)
  296. */
  297. async API_CALL(payload) {
  298. const { url, params } = payload;
  299. let genericApiRequest = await WikiDataModule.GenericApiRequestModel.findOne({
  300. url,
  301. params
  302. });
  303. if (genericApiRequest) return genericApiRequest._doc.responseData;
  304. await WikiDataModule.rateLimiter.continue();
  305. WikiDataModule.rateLimiter.restart();
  306. const { data: responseData } = await WikiDataModule.axios.get(url, {
  307. params,
  308. headers: {
  309. Accept: "application/sparql-results+json"
  310. },
  311. timeout: WikiDataModule.requestTimeout
  312. });
  313. if (responseData.error) throw new Error(responseData.error);
  314. genericApiRequest = new WikiDataModule.GenericApiRequestModel({
  315. url,
  316. params,
  317. responseData,
  318. date: Date.now()
  319. });
  320. genericApiRequest.save();
  321. return responseData;
  322. }
  323. }
  324. export default new _WikiDataModule();