wikidata.js 10.0 KB

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