utils.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005
  1. import config from "config";
  2. import async from "async";
  3. import crypto from "crypto";
  4. import request from "request";
  5. import CoreClass from "../core";
  6. let UtilsModule;
  7. let IOModule;
  8. let SpotifyModule;
  9. let CacheModule;
  10. class _UtilsModule extends CoreClass {
  11. // eslint-disable-next-line require-jsdoc
  12. constructor() {
  13. super("utils");
  14. this.youtubeRequestCallbacks = [];
  15. this.youtubeRequestsPending = 0;
  16. this.youtubeRequestsActive = false;
  17. UtilsModule = this;
  18. }
  19. /**
  20. * Initialises the utils module
  21. *
  22. * @returns {Promise} - returns promise (reject, resolve)
  23. */
  24. initialize() {
  25. return new Promise(resolve => {
  26. IOModule = this.moduleManager.modules.io;
  27. SpotifyModule = this.moduleManager.modules.spotify;
  28. CacheModule = this.moduleManager.modules.cache;
  29. resolve();
  30. });
  31. }
  32. /**
  33. * Parses the cookie into a readable object
  34. *
  35. * @param {object} payload - object that contains the payload
  36. * @param {string} payload.cookieString - the cookie string
  37. * @returns {Promise} - returns promise (reject, resolve)
  38. */
  39. PARSE_COOKIES(payload) {
  40. return new Promise((resolve, reject) => {
  41. const cookies = {};
  42. if (typeof payload.cookieString !== "string") return reject(new Error("Cookie string is not a string"));
  43. // eslint-disable-next-line array-callback-return
  44. payload.cookieString.split("; ").map(cookie => {
  45. cookies[cookie.substring(0, cookie.indexOf("="))] = cookie.substring(
  46. cookie.indexOf("=") + 1,
  47. cookie.length
  48. );
  49. });
  50. return resolve(cookies);
  51. });
  52. }
  53. // COOKIES_TO_STRING() {//cookies
  54. // return new Promise((resolve, reject) => {
  55. // let newCookie = [];
  56. // for (let prop in cookie) {
  57. // newCookie.push(prop + "=" + cookie[prop]);
  58. // }
  59. // return newCookie.join("; ");
  60. // });
  61. // }
  62. /**
  63. * Removes a cookie by name
  64. *
  65. * @param {object} payload - object that contains the payload
  66. * @param {object} payload.cookieString - the cookie string
  67. * @param {string} payload.cookieName - the unique name of the cookie
  68. * @returns {Promise} - returns promise (reject, resolve)
  69. */
  70. REMOVE_COOKIE(payload) {
  71. return new Promise((resolve, reject) => {
  72. let cookies;
  73. try {
  74. cookies = UtilsModule.runJob(
  75. "PARSE_COOKIES",
  76. {
  77. cookieString: payload.cookieString
  78. },
  79. this
  80. );
  81. } catch (err) {
  82. return reject(err);
  83. }
  84. delete cookies[payload.cookieName];
  85. return resolve(this.toString(cookies));
  86. });
  87. }
  88. /**
  89. * Replaces any html reserved characters in a string with html entities
  90. *
  91. * @param {object} payload - object that contains the payload
  92. * @param {string} payload.str - the string to replace characters with html entities
  93. * @returns {Promise} - returns promise (reject, resolve)
  94. */
  95. HTML_ENTITIES(payload) {
  96. return new Promise(resolve => {
  97. resolve(
  98. String(payload.str)
  99. .replace(/&/g, "&")
  100. .replace(/</g, "&lt;")
  101. .replace(/>/g, "&gt;")
  102. .replace(/"/g, "&quot;")
  103. );
  104. });
  105. }
  106. /**
  107. * Generates a random string of a specified length
  108. *
  109. * @param {object} payload - object that contains the payload
  110. * @param {number} payload.length - the length the random string should be
  111. * @returns {Promise} - returns promise (reject, resolve)
  112. */
  113. async GENERATE_RANDOM_STRING(payload) {
  114. const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".split("");
  115. const promises = [];
  116. for (let i = 0; i < payload.length; i += 1) {
  117. promises.push(
  118. UtilsModule.runJob(
  119. "GET_RANDOM_NUMBER",
  120. {
  121. min: 0,
  122. max: chars.length - 1
  123. },
  124. this
  125. )
  126. );
  127. }
  128. const randomNums = await Promise.all(promises);
  129. const randomChars = [];
  130. for (let i = 0; i < payload.length; i += 1) {
  131. randomChars.push(chars[randomNums[i]]);
  132. }
  133. return new Promise(resolve => resolve(randomChars.join("")));
  134. }
  135. /**
  136. * Returns a socket object from a socket identifier
  137. *
  138. * @param {object} payload - object that contains the payload
  139. * @param {string} payload.socketId - the socket id
  140. * @returns {Promise} - returns promise (reject, resolve)
  141. */
  142. async GET_SOCKET_FROM_ID(payload) {
  143. // socketId
  144. const io = await IOModule.runJob("IO", {}, this);
  145. return new Promise(resolve => resolve(io.sockets.sockets[payload.socketId]));
  146. }
  147. /**
  148. * Creates a random number within a range
  149. *
  150. * @param {object} payload - object that contains the payload
  151. * @param {number} payload.min - the minimum number the result should be
  152. * @param {number} payload.max - the maximum number the result should be
  153. * @returns {Promise} - returns promise (reject, resolve)
  154. */
  155. GET_RANDOM_NUMBER(payload) {
  156. // min, max
  157. return new Promise(resolve =>
  158. resolve(Math.floor(Math.random() * (payload.max - payload.min + 1)) + payload.min)
  159. );
  160. }
  161. /**
  162. * Converts ISO8601 time format (YouTube API) to HH:MM:SS
  163. *
  164. * @param {object} payload - object contaiing the payload
  165. * @param {string} payload.duration - string in the format of ISO8601
  166. * @returns {Promise} - returns a promise (resolve, reject)
  167. */
  168. CONVERT_TIME(payload) {
  169. // duration
  170. return new Promise(resolve => {
  171. let { duration } = payload;
  172. let a = duration.match(/\d+/g);
  173. if (duration.indexOf("M") >= 0 && duration.indexOf("H") === -1 && duration.indexOf("S") === -1) {
  174. a = [0, a[0], 0];
  175. }
  176. if (duration.indexOf("H") >= 0 && duration.indexOf("M") === -1) {
  177. a = [a[0], 0, a[1]];
  178. }
  179. if (duration.indexOf("H") >= 0 && duration.indexOf("M") === -1 && duration.indexOf("S") === -1) {
  180. a = [a[0], 0, 0];
  181. }
  182. duration = 0;
  183. if (a.length === 3) {
  184. duration += parseInt(a[0]) * 3600;
  185. duration += parseInt(a[1]) * 60;
  186. duration += parseInt(a[2]);
  187. }
  188. if (a.length === 2) {
  189. duration += parseInt(a[0]) * 60;
  190. duration += parseInt(a[1]);
  191. }
  192. if (a.length === 1) {
  193. duration += parseInt(a[0]);
  194. }
  195. const hours = Math.floor(duration / 3600);
  196. const minutes = Math.floor((duration % 3600) / 60);
  197. const seconds = Math.floor((duration % 3600) % 60);
  198. resolve(
  199. (hours < 10 ? `0${hours}:` : `${hours}:`) +
  200. (minutes < 10 ? `0${minutes}:` : `${minutes}:`) +
  201. (seconds < 10 ? `0${seconds}` : seconds)
  202. );
  203. });
  204. }
  205. /**
  206. * Creates a random identifier for e.g. sessionId
  207. *
  208. * @returns {Promise} - returns promise (reject, resolve)
  209. */
  210. GUID() {
  211. return new Promise(resolve => {
  212. resolve(
  213. [1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1]
  214. .map(b =>
  215. b
  216. ? Math.floor((1 + Math.random()) * 0x10000)
  217. .toString(16)
  218. .substring(1)
  219. : "-"
  220. )
  221. .join("")
  222. );
  223. });
  224. }
  225. // UNKNOWN
  226. // eslint-disable-next-line require-jsdoc
  227. async SOCKET_FROM_SESSION(payload) {
  228. // socketId
  229. const io = await IOModule.runJob("IO", {}, this);
  230. return new Promise((resolve, reject) => {
  231. const ns = io.of("/");
  232. if (ns) {
  233. return resolve(ns.connected[payload.socketId]);
  234. }
  235. return reject();
  236. });
  237. }
  238. /**
  239. * Gets all sockets for a specified session id
  240. *
  241. * @param {object} payload - object containing the payload
  242. * @param {string} payload.sessionId - user session id
  243. * @returns {Promise} - returns promise (reject, resolve)
  244. */
  245. async SOCKETS_FROM_SESSION_ID(payload) {
  246. const io = await IOModule.runJob("IO", {}, this);
  247. return new Promise(resolve => {
  248. const ns = io.of("/");
  249. const sockets = [];
  250. if (ns) {
  251. return async.each(
  252. Object.keys(ns.connected),
  253. (id, next) => {
  254. const { session } = ns.connected[id];
  255. if (session.sessionId === payload.sessionId) sockets.push(session.sessionId);
  256. next();
  257. },
  258. () => {
  259. resolve({ sockets });
  260. }
  261. );
  262. }
  263. return resolve();
  264. });
  265. }
  266. /**
  267. * Returns any sockets for a specific user
  268. *
  269. * @param {object} payload - object that contains the payload
  270. * @param {string} payload.userId - the user id
  271. * @returns {Promise} - returns promise (reject, resolve)
  272. */
  273. async SOCKETS_FROM_USER(payload) {
  274. const io = await IOModule.runJob("IO", {}, this);
  275. return new Promise((resolve, reject) => {
  276. const ns = io.of("/");
  277. const sockets = [];
  278. if (ns) {
  279. return async.each(
  280. Object.keys(ns.connected),
  281. (id, next) => {
  282. const { session } = ns.connected[id];
  283. CacheModule.runJob(
  284. "HGET",
  285. {
  286. table: "sessions",
  287. key: session.sessionId
  288. },
  289. this
  290. )
  291. .then(session => {
  292. if (session && session.userId === payload.userId) sockets.push(ns.connected[id]);
  293. next();
  294. })
  295. .catch(err => {
  296. next(err);
  297. });
  298. },
  299. err => {
  300. if (err) return reject(err);
  301. return resolve({ sockets });
  302. }
  303. );
  304. }
  305. return resolve();
  306. });
  307. }
  308. /**
  309. * Returns any sockets from a specific ip address
  310. *
  311. * @param {object} payload - object that contains the payload
  312. * @param {string} payload.ip - the ip address in question
  313. * @returns {Promise} - returns promise (reject, resolve)
  314. */
  315. async SOCKETS_FROM_IP(payload) {
  316. const io = await IOModule.runJob("IO", {}, this);
  317. return new Promise(resolve => {
  318. const ns = io.of("/");
  319. const sockets = [];
  320. if (ns) {
  321. return async.each(
  322. Object.keys(ns.connected),
  323. (id, next) => {
  324. const { session } = ns.connected[id];
  325. CacheModule.runJob(
  326. "HGET",
  327. {
  328. table: "sessions",
  329. key: session.sessionId
  330. },
  331. this
  332. )
  333. .then(session => {
  334. if (session && ns.connected[id].ip === payload.ip) sockets.push(ns.connected[id]);
  335. next();
  336. })
  337. .catch(() => next());
  338. },
  339. () => {
  340. resolve({ sockets });
  341. }
  342. );
  343. }
  344. return resolve();
  345. });
  346. }
  347. /**
  348. * Returns any sockets from a specific user without using redis/cache
  349. *
  350. * @param {object} payload - object that contains the payload
  351. * @param {string} payload.userId - the id of the user in question
  352. * @returns {Promise} - returns promise (reject, resolve)
  353. */
  354. async SOCKETS_FROM_USER_WITHOUT_CACHE(payload) {
  355. const io = await IOModule.runJob("IO", {}, this);
  356. return new Promise(resolve => {
  357. const ns = io.of("/");
  358. const sockets = [];
  359. if (ns) {
  360. return async.each(
  361. Object.keys(ns.connected),
  362. (id, next) => {
  363. const { session } = ns.connected[id];
  364. if (session.userId === payload.userId) sockets.push(ns.connected[id]);
  365. next();
  366. },
  367. () => {
  368. resolve({ sockets });
  369. }
  370. );
  371. }
  372. return resolve();
  373. });
  374. }
  375. /**
  376. * Allows a socket to leave any rooms they are connected to
  377. *
  378. * @param {object} payload - object that contains the payload
  379. * @param {string} payload.socketId - the id of the socket which should leave all their rooms
  380. * @returns {Promise} - returns promise (reject, resolve)
  381. */
  382. async SOCKET_LEAVE_ROOMS(payload) {
  383. const socket = await UtilsModule.runJob(
  384. "SOCKET_FROM_SESSION",
  385. {
  386. socketId: payload.socketId
  387. },
  388. this
  389. );
  390. return new Promise(resolve => {
  391. const { rooms } = socket;
  392. for (let room = 0, roomKeys = Object.keys(rooms); room < roomKeys.length; room += 1) {
  393. socket.leave(room);
  394. }
  395. return resolve();
  396. });
  397. }
  398. /**
  399. * Allows a socket to join a specified room
  400. *
  401. * @param {object} payload - object that contains the payload
  402. * @param {string} payload.socketId - the id of the socket which should join the room
  403. * @param {object} payload.room - the object representing the room the socket should join
  404. * @returns {Promise} - returns promise (reject, resolve)
  405. */
  406. async SOCKET_JOIN_ROOM(payload) {
  407. const socket = await UtilsModule.runJob(
  408. "SOCKET_FROM_SESSION",
  409. {
  410. socketId: payload.socketId
  411. },
  412. this
  413. );
  414. return new Promise(resolve => {
  415. const { rooms } = socket;
  416. for (let room = 0, roomKeys = Object.keys(rooms); room < roomKeys.length; room += 1) {
  417. socket.leave(room);
  418. }
  419. socket.join(payload.room);
  420. return resolve();
  421. });
  422. }
  423. // UNKNOWN
  424. // eslint-disable-next-line require-jsdoc
  425. async SOCKET_JOIN_SONG_ROOM(payload) {
  426. // socketId, room
  427. const socket = await UtilsModule.runJob(
  428. "SOCKET_FROM_SESSION",
  429. {
  430. socketId: payload.socketId
  431. },
  432. this
  433. );
  434. return new Promise(resolve => {
  435. const { rooms } = socket;
  436. for (let room = 0, roomKeys = Object.keys(rooms); room < roomKeys.length; room += 1) {
  437. if (room.indexOf("song.") !== -1) socket.leave(rooms);
  438. }
  439. socket.join(payload.room);
  440. return resolve();
  441. });
  442. }
  443. // UNKNOWN
  444. // eslint-disable-next-line require-jsdoc
  445. SOCKETS_JOIN_SONG_ROOM(payload) {
  446. // sockets, room
  447. return new Promise(resolve => {
  448. for (let id = 0, socketKeys = Object.keys(payload.sockets); id < socketKeys.length; id += 1) {
  449. const socket = payload.sockets[socketKeys[id]];
  450. const { rooms } = socket;
  451. for (let room = 0, roomKeys = Object.keys(rooms); room < roomKeys.length; room += 1) {
  452. if (room.indexOf("song.") !== -1) socket.leave(room);
  453. }
  454. socket.join(payload.room);
  455. }
  456. return resolve();
  457. });
  458. }
  459. // UNKNOWN
  460. // eslint-disable-next-line require-jsdoc
  461. SOCKETS_LEAVE_SONG_ROOMS(payload) {
  462. // sockets
  463. return new Promise(resolve => {
  464. for (let id = 0, socketKeys = Object.keys(payload.sockets); id < socketKeys.length; id += 1) {
  465. const socket = payload.sockets[socketKeys[id]];
  466. const { rooms } = socket;
  467. for (let room = 0, roomKeys = Object.keys(rooms); room < roomKeys.length; room += 1) {
  468. if (room.indexOf("song.") !== -1) socket.leave(room);
  469. }
  470. }
  471. resolve();
  472. });
  473. }
  474. /**
  475. * Emits arguments to any sockets that are in a specified a room
  476. *
  477. * @param {object} payload - object that contains the payload
  478. * @param {string} payload.room - the name of the room to emit arguments
  479. * @param {object} payload.args - any arguments to be emitted to the sockets in the specific room
  480. * @returns {Promise} - returns promise (reject, resolve)
  481. */
  482. async EMIT_TO_ROOM(payload) {
  483. const io = await IOModule.runJob("IO", {}, this);
  484. return new Promise(resolve => {
  485. const { sockets } = io.sockets;
  486. for (let id = 0, socketKeys = Object.keys(sockets); id < socketKeys.length; id += 1) {
  487. const socket = sockets[socketKeys[id]];
  488. if (socket.rooms[payload.room]) {
  489. socket.emit(...payload.args);
  490. }
  491. }
  492. return resolve();
  493. });
  494. }
  495. /**
  496. * Gets any sockets connected to a room
  497. *
  498. * @param {object} payload - object that contains the payload
  499. * @param {string} payload.room - the name of the room
  500. * @returns {Promise} - returns promise (reject, resolve)
  501. */
  502. async GET_ROOM_SOCKETS(payload) {
  503. const io = await IOModule.runJob("IO", {}, this);
  504. return new Promise(resolve => {
  505. const { sockets } = io.sockets;
  506. const roomSockets = [];
  507. for (let id = 0, socketKeys = Object.keys(sockets); id < socketKeys.length; id += 1) {
  508. const socket = sockets[socketKeys[id]];
  509. if (socket.rooms[payload.room]) roomSockets.push(socket);
  510. }
  511. return resolve(roomSockets);
  512. });
  513. }
  514. /**
  515. * Gets the details of a song using the YouTube API
  516. *
  517. * @param {object} payload - object that contains the payload
  518. * @param {string} payload.songId - the YouTube API id of the song
  519. * @returns {Promise} - returns promise (reject, resolve)
  520. */
  521. GET_SONG_FROM_YOUTUBE(payload) {
  522. // songId, cb
  523. return new Promise((resolve, reject) => {
  524. this.youtubeRequestCallbacks.push({
  525. cb: () => {
  526. this.youtubeRequestsActive = true;
  527. const youtubeParams = [
  528. "part=snippet,contentDetails,statistics,status",
  529. `id=${encodeURIComponent(payload.songId)}`,
  530. `key=${config.get("apis.youtube.key")}`
  531. ].join("&");
  532. request(`https://www.googleapis.com/youtube/v3/videos?${youtubeParams}`, (err, res, body) => {
  533. this.youtubeRequestCallbacks.splice(0, 1);
  534. if (this.youtubeRequestCallbacks.length > 0) {
  535. this.youtubeRequestCallbacks[0].cb(this.youtubeRequestCallbacks[0].songId);
  536. } else this.youtubeRequestsActive = false;
  537. if (err) {
  538. console.error(err);
  539. return null;
  540. }
  541. body = JSON.parse(body);
  542. if (body.error) {
  543. console.log("ERROR", "GET_SONG_FROM_YOUTUBE", `${body.error.message}`);
  544. return reject(new Error("An error has occured. Please try again later."));
  545. }
  546. if (body.items[0] === undefined)
  547. return reject(
  548. new Error("The specified video does not exist or cannot be publicly accessed.")
  549. );
  550. // TODO Clean up duration converter
  551. let dur = body.items[0].contentDetails.duration;
  552. dur = dur.replace("PT", "");
  553. let duration = 0;
  554. dur = dur.replace(/([\d]*)H/, (v, v2) => {
  555. v2 = Number(v2);
  556. duration = v2 * 60 * 60;
  557. return "";
  558. });
  559. dur = dur.replace(/([\d]*)M/, (v, v2) => {
  560. v2 = Number(v2);
  561. duration += v2 * 60;
  562. return "";
  563. });
  564. // eslint-disable-next-line no-unused-vars
  565. dur = dur.replace(/([\d]*)S/, (v, v2) => {
  566. v2 = Number(v2);
  567. duration += v2;
  568. return "";
  569. });
  570. const song = {
  571. songId: body.items[0].id,
  572. title: body.items[0].snippet.title,
  573. duration
  574. };
  575. return resolve({ song });
  576. });
  577. },
  578. songId: payload.songId
  579. });
  580. if (!this.youtubeRequestsActive) {
  581. this.youtubeRequestCallbacks[0].cb(this.youtubeRequestCallbacks[0].songId);
  582. }
  583. });
  584. }
  585. /**
  586. * Filters a list of YouTube videos so that they only contains videos with music
  587. *
  588. * @param {object} payload - object that contains the payload
  589. * @param {Array} payload.videoIds - an array of YouTube videoIds to filter through
  590. * @returns {Promise} - returns promise (reject, resolve)
  591. */
  592. FILTER_MUSIC_VIDEOS_YOUTUBE(payload) {
  593. // videoIds, cb
  594. return new Promise((resolve, reject) => {
  595. /**
  596. * @param {Function} cb2 - callback
  597. */
  598. function getNextPage(cb2) {
  599. const localVideoIds = payload.videoIds.splice(0, 50);
  600. const youtubeParams = [
  601. "part=topicDetails",
  602. `id=${encodeURIComponent(localVideoIds.join(","))}`,
  603. `maxResults=50`,
  604. `key=${config.get("apis.youtube.key")}`
  605. ].join("&");
  606. request(`https://www.googleapis.com/youtube/v3/videos?${youtubeParams}`, (err, res, body) => {
  607. if (err) {
  608. console.error(err);
  609. return reject(new Error("Failed to find playlist from YouTube"));
  610. }
  611. body = JSON.parse(body);
  612. if (body.error) {
  613. console.log("ERROR", "FILTER_MUSIC_VIDEOS_YOUTUBE", `${body.error.message}`);
  614. return reject(new Error("An error has occured. Please try again later."));
  615. }
  616. const songIds = [];
  617. body.items.forEach(item => {
  618. const songId = item.id;
  619. if (!item.topicDetails) return;
  620. if (item.topicDetails.relevantTopicIds.indexOf("/m/04rlf") !== -1) {
  621. songIds.push(songId);
  622. }
  623. });
  624. if (payload.videoIds.length > 0) {
  625. return getNextPage(newSongIds => {
  626. cb2(songIds.concat(newSongIds));
  627. });
  628. }
  629. return cb2(songIds);
  630. });
  631. }
  632. if (payload.videoIds.length === 0) resolve({ songIds: [] });
  633. else getNextPage(songIds => resolve({ songIds }));
  634. });
  635. }
  636. /**
  637. * Returns an array of songs taken from a YouTube playlist
  638. *
  639. * @param {object} payload - object that contains the payload
  640. * @param {boolean} payload.musicOnly - whether to return music videos or all videos in the playlist
  641. * @param {string} payload.url - the url of the YouTube playlist
  642. * @returns {Promise} - returns promise (reject, resolve)
  643. */
  644. GET_PLAYLIST_FROM_YOUTUBE(payload) {
  645. // payload includes: url, musicOnly
  646. return new Promise((resolve, reject) => {
  647. const local = this;
  648. const name = "list".replace(/[\\[]/, "\\[").replace(/[\]]/, "\\]");
  649. const regex = new RegExp(`[\\?&]${name}=([^&#]*)`);
  650. const splitQuery = regex.exec(payload.url);
  651. if (!splitQuery) {
  652. console.log("ERROR", "GET_PLAYLIST_FROM_YOUTUBE", "Invalid YouTube playlist URL query.");
  653. return reject(new Error("An error has occured. Please try again later."));
  654. }
  655. const playlistId = splitQuery[1];
  656. /**
  657. * @param {string} pageToken - page token for YouTube API
  658. * @param {Array} songs - array of songs
  659. */
  660. function getPage(pageToken, songs) {
  661. const nextPageToken = pageToken ? `pageToken=${pageToken}` : "";
  662. const youtubeParams = [
  663. "part=contentDetails",
  664. `playlistId=${encodeURIComponent(playlistId)}`,
  665. `maxResults=50`,
  666. `key=${config.get("apis.youtube.key")}`,
  667. nextPageToken
  668. ].join("&");
  669. request(
  670. `https://www.googleapis.com/youtube/v3/playlistItems?${youtubeParams}`,
  671. async (err, res, body) => {
  672. if (err) {
  673. console.error(err);
  674. return reject(new Error("Failed to find playlist from YouTube"));
  675. }
  676. body = JSON.parse(body);
  677. if (body.error) {
  678. console.log("ERROR", "GET_PLAYLIST_FROM_YOUTUBE", `${body.error.message}`);
  679. return reject(new Error("An error has occured. Please try again later."));
  680. }
  681. songs = songs.concat(body.items);
  682. if (body.nextPageToken) return getPage(body.nextPageToken, songs);
  683. songs = songs.map(song => song.contentDetails.videoId);
  684. if (!payload.musicOnly) return resolve({ songs });
  685. return local
  686. .runJob(
  687. "FILTER_MUSIC_VIDEOS_YOUTUBE",
  688. {
  689. videoIds: songs.slice()
  690. },
  691. this
  692. )
  693. .then(filteredSongs => {
  694. resolve({ filteredSongs, songs });
  695. });
  696. }
  697. );
  698. }
  699. return getPage(null, []);
  700. });
  701. }
  702. /**
  703. * Gets the details of a song from the Spotify API
  704. *
  705. * @param {object} payload - object that contains the payload
  706. * @param {object} payload.song - the song object (song.title etc.)
  707. * @returns {Promise} - returns promise (reject, resolve)
  708. */
  709. async GET_SONG_FROM_SPOTIFY(payload) {
  710. // song
  711. const token = await SpotifyModule.runJob("GET_TOKEN", {}, this);
  712. return new Promise((resolve, reject) => {
  713. if (!config.get("apis.spotify.enabled")) return reject(new Error("Spotify is not enabled."));
  714. const song = { ...payload.song };
  715. const spotifyParams = [`q=${encodeURIComponent(payload.song.title)}`, `type=track`].join("&");
  716. const options = {
  717. url: `https://api.spotify.com/v1/search?${spotifyParams}`,
  718. headers: {
  719. Authorization: `Bearer ${token}`
  720. }
  721. };
  722. return request(options, (err, res, body) => {
  723. if (err) console.error(err);
  724. body = JSON.parse(body);
  725. if (body.error) console.error(body.error);
  726. for (let i = 0, bodyKeys = Object.keys(body); i < bodyKeys.length; i += 1) {
  727. const { items } = body[i];
  728. for (let j = 0, itemKeys = Object.keys(body); j < itemKeys.length; j += 1) {
  729. const item = items[j];
  730. let hasArtist = false;
  731. for (let k = 0; k < item.artists.length; k += 1) {
  732. const artist = item.artists[k];
  733. if (song.title.indexOf(artist.name) !== -1) hasArtist = true;
  734. }
  735. if (hasArtist && song.title.indexOf(item.name) !== -1) {
  736. song.duration = item.duration_ms / 1000;
  737. song.artists = item.artists.map(artist => artist.name);
  738. song.title = item.name;
  739. song.explicit = item.explicit;
  740. song.thumbnail = item.album.images[1].url;
  741. break;
  742. }
  743. }
  744. }
  745. resolve({ song });
  746. });
  747. });
  748. }
  749. /**
  750. * Returns the details of multiple songs from the Spotify API
  751. *
  752. * @param {object} payload - object that contains the payload
  753. * @param {object} payload.title - the query/title of a song to search the API with
  754. * @returns {Promise} - returns promise (reject, resolve)
  755. */
  756. async GET_SONGS_FROM_SPOTIFY(payload) {
  757. // title, artist
  758. const token = await SpotifyModule.runJob("GET_TOKEN", {}, this);
  759. return new Promise((resolve, reject) => {
  760. if (!config.get("apis.spotify.enabled")) return reject(new Error("Spotify is not enabled."));
  761. const spotifyParams = [`q=${encodeURIComponent(payload.title)}`, `type=track`].join("&");
  762. const options = {
  763. url: `https://api.spotify.com/v1/search?${spotifyParams}`,
  764. headers: {
  765. Authorization: `Bearer ${token}`
  766. }
  767. };
  768. return request(options, (err, res, body) => {
  769. if (err) return console.error(err);
  770. body = JSON.parse(body);
  771. if (body.error) return console.error(body.error);
  772. const songs = [];
  773. for (let i = 0, bodyKeys = Object.keys(body); i < bodyKeys.length; i += 1) {
  774. const { items } = body[i];
  775. for (let j = 0, itemKeys = Object.keys(body); j < itemKeys.length; j += 1) {
  776. const item = items[j];
  777. let hasArtist = false;
  778. for (let k = 0; k < item.artists.length; k += 1) {
  779. const localArtist = item.artists[k];
  780. if (payload.artist.toLowerCase() === localArtist.name.toLowerCase()) hasArtist = true;
  781. }
  782. if (
  783. hasArtist &&
  784. (payload.title.indexOf(item.name) !== -1 || item.name.indexOf(payload.title) !== -1)
  785. ) {
  786. const song = {};
  787. song.duration = item.duration_ms / 1000;
  788. song.artists = item.artists.map(artist => artist.name);
  789. song.title = item.name;
  790. song.explicit = item.explicit;
  791. song.thumbnail = item.album.images[1].url;
  792. songs.push(song);
  793. }
  794. }
  795. }
  796. return resolve({ songs });
  797. });
  798. });
  799. }
  800. /**
  801. * Shuffles an array of songs
  802. *
  803. * @param {object} payload - object that contains the payload
  804. * @param {object} payload.array - an array of songs that should be shuffled
  805. * @returns {Promise} - returns promise (reject, resolve)
  806. */
  807. SHUFFLE(payload) {
  808. // array
  809. return new Promise(resolve => {
  810. const array = payload.array.slice();
  811. let currentIndex = payload.array.length;
  812. let temporaryValue;
  813. let randomIndex;
  814. // While there remain elements to shuffle...
  815. while (currentIndex !== 0) {
  816. // Pick a remaining element...
  817. randomIndex = Math.floor(Math.random() * currentIndex);
  818. currentIndex -= 1;
  819. // And swap it with the current element.
  820. temporaryValue = array[currentIndex];
  821. array[currentIndex] = array[randomIndex];
  822. array[randomIndex] = temporaryValue;
  823. }
  824. resolve({ array });
  825. });
  826. }
  827. /**
  828. * Creates an error
  829. *
  830. * @param {object} payload - object that contains the payload
  831. * @param {object} payload.error - object that contains the error
  832. * @param {string} payload.message - possible error message
  833. * @param {object} payload.errors - possible object that contains multiple errors
  834. * @returns {Promise} - returns promise (reject, resolve)
  835. */
  836. GET_ERROR(payload) {
  837. return new Promise(resolve => {
  838. let error = "An error occurred.";
  839. if (typeof payload.error === "string") error = payload.error;
  840. else if (payload.error.message) {
  841. if (payload.error.message !== "Validation failed") error = payload.error.message;
  842. else error = payload.error.errors[Object.keys(payload.error.errors)].message;
  843. }
  844. resolve(error);
  845. });
  846. }
  847. /**
  848. * Creates the gravatar url for a specified email address
  849. *
  850. * @param {object} payload - object that contains the payload
  851. * @param {string} payload.email - the email address
  852. * @returns {Promise} - returns promise (reject, resolve)
  853. */
  854. CREATE_GRAVATAR(payload) {
  855. return new Promise(resolve => {
  856. const hash = crypto.createHash("md5").update(payload.email).digest("hex");
  857. resolve(`https://www.gravatar.com/avatar/${hash}`);
  858. });
  859. }
  860. /**
  861. * @returns {Promise} - returns promise (reject, resolve)
  862. */
  863. DEBUG() {
  864. return new Promise(resolve => resolve());
  865. }
  866. }
  867. export default new _UtilsModule();