apis.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. import config from "config";
  2. import async from "async";
  3. import axios from "axios";
  4. import isLoginRequired from "../hooks/loginRequired";
  5. import { hasPermission, useHasPermission } from "../hooks/hasPermission";
  6. // eslint-disable-next-line
  7. import moduleManager from "../../index";
  8. const UtilsModule = moduleManager.modules.utils;
  9. const WSModule = moduleManager.modules.ws;
  10. const YouTubeModule = moduleManager.modules.youtube;
  11. const SpotifyModule = moduleManager.modules.spotify;
  12. const CacheModule = moduleManager.modules.cache;
  13. export default {
  14. /**
  15. * Fetches a list of songs from Youtube's API
  16. *
  17. * @param {object} session - user session
  18. * @param {string} query - the query we'll pass to youtubes api
  19. * @param {Function} cb - callback
  20. * @returns {{status: string, data: object}} - returns an object
  21. */
  22. searchYoutube: isLoginRequired(function searchYoutube(session, query, cb) {
  23. return YouTubeModule.runJob("SEARCH", { query }, this)
  24. .then(data => {
  25. this.log("SUCCESS", "APIS_SEARCH_YOUTUBE", `Searching YouTube successful with query "${query}".`);
  26. return cb({ status: "success", data });
  27. })
  28. .catch(async err => {
  29. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  30. this.log("ERROR", "APIS_SEARCH_YOUTUBE", `Searching youtube failed with query "${query}". "${err}"`);
  31. return cb({ status: "error", message: err });
  32. });
  33. }),
  34. /**
  35. * Fetches a specific page of search results from Youtube's API
  36. *
  37. * @param {object} session - user session
  38. * @param {string} query - the query we'll pass to youtubes api
  39. * @param {string} pageToken - identifies a specific page in the result set that should be retrieved
  40. * @param {Function} cb - callback
  41. * @returns {{status: string, data: object}} - returns an object
  42. */
  43. searchYoutubeForPage: isLoginRequired(function searchYoutubeForPage(session, query, pageToken, cb) {
  44. return YouTubeModule.runJob("SEARCH", { query, pageToken }, this)
  45. .then(data => {
  46. this.log(
  47. "SUCCESS",
  48. "APIS_SEARCH_YOUTUBE_FOR_PAGE",
  49. `Searching YouTube successful with query "${query}".`
  50. );
  51. return cb({ status: "success", data });
  52. })
  53. .catch(async err => {
  54. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  55. this.log(
  56. "ERROR",
  57. "APIS_SEARCH_YOUTUBE_FOR_PAGE",
  58. `Searching youtube failed with query "${query}". "${err}"`
  59. );
  60. return cb({ status: "error", message: err });
  61. });
  62. }),
  63. /**
  64. * Gets Discogs data
  65. *
  66. * @param session
  67. * @param query - the query
  68. * @param {Function} cb
  69. */
  70. searchDiscogs: useHasPermission("apis.searchDiscogs", function searchDiscogs(session, query, page, cb) {
  71. async.waterfall(
  72. [
  73. next => {
  74. const options = {
  75. params: { q: query, per_page: 20, page },
  76. headers: {
  77. "User-Agent": "Request",
  78. Authorization: `Discogs key=${config.get("apis.discogs.client")}, secret=${config.get(
  79. "apis.discogs.secret"
  80. )}`
  81. }
  82. };
  83. axios
  84. .get("https://api.discogs.com/database/search", options)
  85. .then(res => next(null, res.data))
  86. .catch(err => next(err));
  87. }
  88. ],
  89. async (err, body) => {
  90. if (err) {
  91. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  92. this.log(
  93. "ERROR",
  94. "APIS_SEARCH_DISCOGS",
  95. `Searching discogs failed with query "${query}". "${err}"`
  96. );
  97. return cb({ status: "error", message: err });
  98. }
  99. this.log(
  100. "SUCCESS",
  101. "APIS_SEARCH_DISCOGS",
  102. `User "${session.userId}" searched Discogs succesfully for query "${query}".`
  103. );
  104. return cb({
  105. status: "success",
  106. data: {
  107. results: body.results,
  108. pages: body.pagination.pages
  109. }
  110. });
  111. }
  112. );
  113. }),
  114. // /**
  115. // *
  116. // *
  117. // * @param session
  118. // * @param ISRC - the ISRC
  119. // * @param {Function} cb
  120. // */
  121. // searchMusicBrainzISRC: useHasPermission("admin.view.spotify", function searchMusicBrainzISRC(session, ISRC, cb) {
  122. // async.waterfall(
  123. // [
  124. // next => {
  125. // if (!ISRC) {
  126. // next("Invalid ISRC provided.");
  127. // return;
  128. // }
  129. // CacheModule.runJob("HGET", { table: "musicbrainz-isrc-2", key: ISRC })
  130. // .then(response => {
  131. // if (response) next(null, response);
  132. // else next(null, null);
  133. // })
  134. // .catch(err => {
  135. // next(err);
  136. // });
  137. // },
  138. // (body, next) => {
  139. // if (body) {
  140. // next(null, body);
  141. // return;
  142. // }
  143. // const options = {
  144. // params: { fmt: "json", inc: "url-rels+work-rels" },
  145. // headers: {
  146. // "User-Agent": "Musare/3.9.0-fork ( https://git.kvos.dev/kris/MusareFork )" // TODO set this in accordance to https://musicbrainz.org/doc/MusicBrainz_API/Rate_Limiting
  147. // }
  148. // };
  149. // console.log("KRIS101", options, `https://musicbrainz.org/ws/2/isrc/${ISRC}`);
  150. // axios
  151. // .get(`https://musicbrainz.org/ws/2/isrc/${ISRC}`, options)
  152. // .then(res => next(null, res.data))
  153. // .catch(err => next(err));
  154. // },
  155. // (body, next) => {
  156. // console.log("KRIS222", body);
  157. // CacheModule.runJob("HSET", { table: "musicbrainz-isrc-2", key: ISRC, value: body })
  158. // .then(() => {})
  159. // .catch(() => {});
  160. // next(null, body);
  161. // },
  162. // (body, next) => {
  163. // const response = {};
  164. // const recordingUrls = Array.from(
  165. // new Set(
  166. // body.recordings
  167. // .map(recording =>
  168. // recording.relations
  169. // .filter(
  170. // relation =>
  171. // relation["target-type"] === "url" &&
  172. // relation.url &&
  173. // // relation["type-id"] === "7e41ef12-a124-4324-afdb-fdbae687a89c" &&
  174. // (relation.url.resource.indexOf("youtu.be") !== -1 ||
  175. // relation.url.resource.indexOf("youtube.com") !== -1 ||
  176. // relation.url.resource.indexOf("soundcloud.com") !== -1)
  177. // )
  178. // .map(relation => relation.url.resource)
  179. // )
  180. // .flat()
  181. // )
  182. // );
  183. // const workIds = Array.from(
  184. // new Set(
  185. // body.recordings
  186. // .map(recording =>
  187. // recording.relations
  188. // .filter(relation => relation["target-type"] === "work" && relation.work)
  189. // .map(relation => relation.work.id)
  190. // )
  191. // .flat()
  192. // )
  193. // );
  194. // response.recordingUrls = recordingUrls;
  195. // response.workIds = workIds;
  196. // response.raw = body;
  197. // next(null, response);
  198. // }
  199. // ],
  200. // async (err, response) => {
  201. // if (err && err !== true) {
  202. // err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  203. // this.log(
  204. // "ERROR",
  205. // "APIS_SEARCH_MUSICBRAINZ_ISRC",
  206. // `Searching MusicBrainz ISRC failed with ISRC "${ISRC}". "${err}"`
  207. // );
  208. // return cb({ status: "error", message: err });
  209. // }
  210. // this.log(
  211. // "SUCCESS",
  212. // "APIS_SEARCH_MUSICBRAINZ_ISRC",
  213. // `User "${session.userId}" searched MusicBrainz ISRC succesfully for ISRC "${ISRC}".`
  214. // );
  215. // return cb({
  216. // status: "success",
  217. // data: {
  218. // response
  219. // }
  220. // });
  221. // }
  222. // );
  223. // }),
  224. // /**
  225. // *
  226. // *
  227. // * @param session
  228. // * @param trackId - the trackId
  229. // * @param {Function} cb
  230. // */
  231. // searchWikidataBySpotifyTrackId: useHasPermission(
  232. // "admin.view.spotify",
  233. // function searchWikidataBySpotifyTrackId(session, trackId, cb) {
  234. // async.waterfall(
  235. // [
  236. // next => {
  237. // if (!trackId) {
  238. // next("Invalid trackId provided.");
  239. // return;
  240. // }
  241. // CacheModule.runJob("HGET", { table: "wikidata-spotify-track", key: trackId })
  242. // .then(response => {
  243. // if (response) next(null, response);
  244. // else next(null, null);
  245. // })
  246. // .catch(err => {
  247. // console.log("WOW", err);
  248. // next(err);
  249. // });
  250. // },
  251. // (body, next) => {
  252. // if (body) {
  253. // next(null, body);
  254. // return;
  255. // }
  256. // // const options = {
  257. // // params: { fmt: "json", inc: "url-rels" },
  258. // // headers: {
  259. // // "User-Agent": "Musare/3.9.0-fork ( https://git.kvos.dev/kris/MusareFork )" // TODO set this in accordance to https://musicbrainz.org/doc/MusicBrainz_API/Rate_Limiting
  260. // // }
  261. // // };
  262. // // axios
  263. // // .get(`https://musicbrainz.org/ws/2/isrc/${ISRC}`, options)
  264. // // .then(res => next(null, res.data))
  265. // // .catch(err => next(err));
  266. // },
  267. // (body, next) => {
  268. // CacheModule.runJob("HSET", { table: "musicbrainz-isrc", key: ISRC, value: body })
  269. // .then(() => {})
  270. // .catch(() => {});
  271. // next(null, body);
  272. // },
  273. // (body, next) => {
  274. // const response = {};
  275. // const recordingUrls = Array.from(
  276. // new Set(
  277. // body.recordings
  278. // .map(recording =>
  279. // recording.relations
  280. // .filter(
  281. // relation =>
  282. // relation["target-type"] === "url" &&
  283. // relation.url &&
  284. // // relation["type-id"] === "7e41ef12-a124-4324-afdb-fdbae687a89c" &&
  285. // (relation.url.resource.indexOf("youtu.be") !== -1 ||
  286. // relation.url.resource.indexOf("youtube.com") !== -1 ||
  287. // relation.url.resource.indexOf("soundcloud.com") !== -1)
  288. // )
  289. // .map(relation => relation.url.resource)
  290. // )
  291. // .flat()
  292. // )
  293. // );
  294. // response.recordingUrls = recordingUrls;
  295. // response.raw = body;
  296. // next(null, response);
  297. // }
  298. // ],
  299. // async (err, response) => {
  300. // if (err && err !== true) {
  301. // err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  302. // this.log(
  303. // "ERROR",
  304. // "APIS_SEARCH_TODO",
  305. // `Searching MusicBrainz ISRC failed with ISRC "${ISRC}". "${err}"`
  306. // );
  307. // return cb({ status: "error", message: err });
  308. // }
  309. // this.log(
  310. // "SUCCESS",
  311. // "APIS_SEARCH_TODO",
  312. // `User "${session.userId}" searched MusicBrainz ISRC succesfully for ISRC "${ISRC}".`
  313. // );
  314. // return cb({
  315. // status: "success",
  316. // data: {
  317. // response
  318. // }
  319. // });
  320. // }
  321. // );
  322. // }
  323. // ),
  324. // /**
  325. // *
  326. // *
  327. // * @param session
  328. // * @param trackId - the trackId
  329. // * @param {Function} cb
  330. // */
  331. // searchWikidataByMusicBrainzWorkId: useHasPermission(
  332. // "admin.view.spotify",
  333. // function searchWikidataByMusicBrainzWorkId(session, workId, cb) {
  334. // async.waterfall(
  335. // [
  336. // next => {
  337. // if (!workId) {
  338. // next("Invalid workId provided.");
  339. // return;
  340. // }
  341. // CacheModule.runJob("HGET", { table: "wikidata-musicbrainz-work", key: workId })
  342. // .then(response => {
  343. // if (response) next(null, response);
  344. // else next(null, null);
  345. // })
  346. // .catch(err => {
  347. // next(err);
  348. // });
  349. // },
  350. // (body, next) => {
  351. // if (body) {
  352. // next(null, body);
  353. // return;
  354. // }
  355. // const endpointUrl = "https://query.wikidata.org/sparql";
  356. // const sparqlQuery = `SELECT DISTINCT ?item ?itemLabel ?YouTube_video_ID WHERE {
  357. // SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE]". }
  358. // {
  359. // SELECT DISTINCT ?item WHERE {
  360. // ?item p:P435 ?statement0.
  361. // ?statement0 ps:P435 "${workId}".
  362. // }
  363. // LIMIT 100
  364. // }
  365. // OPTIONAL { ?item wdt:P1651 ?YouTube_video_ID. }
  366. // }`;
  367. // // OPTIONAL { ?item wdt:P3040 ?SoundCloud_track_ID. }
  368. // const options = {
  369. // params: { query: sparqlQuery },
  370. // headers: {
  371. // Accept: "application/sparql-results+json"
  372. // }
  373. // };
  374. // axios
  375. // .get(endpointUrl, options)
  376. // .then(res => next(null, res.data))
  377. // .catch(err => next(err));
  378. // },
  379. // (body, next) => {
  380. // CacheModule.runJob("HSET", { table: "wikidata-musicbrainz-work", key: workId, value: body })
  381. // .then(() => {})
  382. // .catch(() => {});
  383. // next(null, body);
  384. // },
  385. // (body, next) => {
  386. // const response = {};
  387. // const youtubeIds = Array.from(
  388. // new Set(
  389. // body.results.bindings
  390. // .filter(binding => !!binding.YouTube_video_ID)
  391. // .map(binding => binding.YouTube_video_ID.value)
  392. // )
  393. // );
  394. // // const soundcloudIds = Array.from(new Set(body.results.bindings.filter(binding => !!binding["SoundCloud_track_ID"]).map(binding => binding["SoundCloud_track_ID"].value)))
  395. // response.youtubeIds = youtubeIds;
  396. // response.raw = body;
  397. // next(null, response);
  398. // }
  399. // ],
  400. // async (err, response) => {
  401. // if (err && err !== true) {
  402. // err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  403. // this.log(
  404. // "ERROR",
  405. // "APIS_SEARCH_TODO",
  406. // `Searching MusicBrainz ISRC failed with ISRC "${workId}". "${err}"`
  407. // );
  408. // return cb({ status: "error", message: err });
  409. // }
  410. // this.log(
  411. // "SUCCESS",
  412. // "APIS_SEARCH_TODO",
  413. // `User "${session.userId}" searched MusicBrainz ISRC succesfully for ISRC "${workId}".`
  414. // );
  415. // return cb({
  416. // status: "success",
  417. // data: {
  418. // response
  419. // }
  420. // });
  421. // }
  422. // );
  423. // }
  424. // ),
  425. /**
  426. *
  427. *
  428. * @param session
  429. * @param trackId - the trackId
  430. * @param {Function} cb
  431. */
  432. getAlternativeMediaSourcesForTracks: useHasPermission(
  433. "admin.view.spotify",
  434. function getAlternativeMediaSourcesForTracks(session, mediaSources, collectAlternativeMediaSourcesOrigins, cb) {
  435. async.waterfall(
  436. [
  437. next => {
  438. if (!mediaSources) {
  439. next("Invalid mediaSources provided.");
  440. return;
  441. }
  442. next();
  443. },
  444. async () => {
  445. this.keepLongJob();
  446. this.publishProgress({
  447. status: "started",
  448. title: "Getting alternative media sources for Spotify tracks",
  449. message: "Starting up",
  450. id: this.toString()
  451. });
  452. console.log("KRIS@4", this.toString());
  453. // await CacheModule.runJob(
  454. // "RPUSH",
  455. // { key: `longJobs.${session.userId}`, value: this.toString() },
  456. // this
  457. // );
  458. SpotifyModule.runJob(
  459. "GET_ALTERNATIVE_MEDIA_SOURCES_FOR_TRACKS",
  460. { mediaSources, collectAlternativeMediaSourcesOrigins },
  461. this
  462. );
  463. }
  464. ],
  465. async err => {
  466. if (err) {
  467. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  468. this.log(
  469. "ERROR",
  470. "APIS_GET_ALTERNATIVE_SOURCES",
  471. `Getting alternative sources failed for "${mediaSources.join(", ")}". "${err}"`
  472. );
  473. return cb({ status: "error", message: err });
  474. }
  475. this.log(
  476. "SUCCESS",
  477. "APIS_GET_ALTERNATIVE_SOURCES",
  478. `User "${session.userId}" started getting alternatives for "${mediaSources.join(", ")}".`
  479. );
  480. return cb({
  481. status: "success"
  482. });
  483. }
  484. );
  485. }
  486. ),
  487. /**
  488. * Joins a room
  489. *
  490. * @param {object} session - user session
  491. * @param {string} room - the room to join
  492. * @param {Function} cb - callback
  493. */
  494. joinRoom(session, room, cb) {
  495. const roomName = room.split(".")[0];
  496. // const roomId = room.split(".")[1];
  497. const rooms = {
  498. home: null,
  499. news: null,
  500. profile: null,
  501. "view-youtube-video": null,
  502. "manage-station": null,
  503. // "manage-station": "stations.view",
  504. "edit-song": "songs.update",
  505. "edit-songs": "songs.update",
  506. "import-album": "songs.update",
  507. // "edit-playlist": "playlists.update",
  508. "view-report": "reports.get",
  509. "edit-user": "users.update",
  510. "view-api-request": "youtube.getApiRequest",
  511. "view-punishment": "punishments.get"
  512. };
  513. const join = (status, error) => {
  514. if (status === "success")
  515. WSModule.runJob("SOCKET_JOIN_ROOM", {
  516. socketId: session.socketId,
  517. room
  518. })
  519. .then(() => cb({ status: "success", message: "Successfully joined room." }))
  520. .catch(err => join("error", err.message));
  521. else {
  522. this.log("ERROR", `Joining room failed: ${error}`);
  523. cb({ status: "error", message: error });
  524. }
  525. };
  526. if (rooms[roomName] === null) join("success");
  527. else if (rooms[roomName])
  528. hasPermission(rooms[roomName], session)
  529. .then(() => join("success"))
  530. .catch(err => join("error", err));
  531. else join("error", "Room not found");
  532. },
  533. /**
  534. * Leaves a room
  535. *
  536. * @param {object} session - user session
  537. * @param {string} room - the room to leave
  538. * @param {Function} cb - callback
  539. */
  540. leaveRoom(session, room, cb) {
  541. if (
  542. room === "home" ||
  543. room.startsWith("profile.") ||
  544. room.startsWith("manage-station.") ||
  545. room.startsWith("edit-song.") ||
  546. room.startsWith("view-report.") ||
  547. room === "import-album" ||
  548. room === "edit-songs"
  549. ) {
  550. WSModule.runJob("SOCKET_LEAVE_ROOM", {
  551. socketId: session.socketId,
  552. room
  553. })
  554. .then(() => {})
  555. .catch(err => {
  556. this.log("ERROR", `Leaving room failed: ${err.message}`);
  557. });
  558. }
  559. cb({ status: "success", message: "Successfully left room." });
  560. },
  561. /**
  562. * Joins an admin room
  563. *
  564. * @param {object} session - user session
  565. * @param {string} page - the admin room to join
  566. * @param {Function} cb - callback
  567. */
  568. joinAdminRoom(session, page, cb) {
  569. if (
  570. page === "songs" ||
  571. page === "stations" ||
  572. page === "reports" ||
  573. page === "news" ||
  574. page === "playlists" ||
  575. page === "users" ||
  576. page === "statistics" ||
  577. page === "punishments" ||
  578. page === "youtube" ||
  579. page === "youtubeVideos" ||
  580. page === "soundcloud" ||
  581. page === "soundcloudTracks" ||
  582. page === "import" ||
  583. page === "dataRequests"
  584. ) {
  585. hasPermission(`admin.view.${page}`, session.userId)
  586. .then(() =>
  587. WSModule.runJob("SOCKET_LEAVE_ROOMS", { socketId: session.socketId }).then(() => {
  588. WSModule.runJob(
  589. "SOCKET_JOIN_ROOM",
  590. {
  591. socketId: session.socketId,
  592. room: `admin.${page}`
  593. },
  594. this
  595. ).then(() => cb({ status: "success", message: "Successfully joined admin room." }));
  596. })
  597. )
  598. .catch(() => cb({ status: "error", message: "Failed to join admin room." }));
  599. }
  600. },
  601. /**
  602. * Leaves all rooms
  603. *
  604. * @param {object} session - user session
  605. * @param {Function} cb - callback
  606. */
  607. leaveRooms(session, cb) {
  608. WSModule.runJob("SOCKET_LEAVE_ROOMS", { socketId: session.socketId });
  609. cb({ status: "success", message: "Successfully left all rooms." });
  610. },
  611. /**
  612. * Returns current date
  613. *
  614. * @param {object} session - user session
  615. * @param {Function} cb - callback
  616. */
  617. ping(session, cb) {
  618. cb({ status: "success", message: "Successfully pinged.", data: { date: Date.now() } });
  619. }
  620. };