songs.js 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365
  1. import async from "async";
  2. import { isAdminRequired, isLoginRequired } from "./hooks";
  3. import moduleManager from "../../index";
  4. const DBModule = moduleManager.modules.db;
  5. const UtilsModule = moduleManager.modules.utils;
  6. const WSModule = moduleManager.modules.ws;
  7. const CacheModule = moduleManager.modules.cache;
  8. const SongsModule = moduleManager.modules.songs;
  9. const ActivitiesModule = moduleManager.modules.activities;
  10. const YouTubeModule = moduleManager.modules.youtube;
  11. const PlaylistsModule = moduleManager.modules.playlists;
  12. CacheModule.runJob("SUB", {
  13. channel: "song.updated",
  14. cb: async data => {
  15. const songModel = await DBModule.runJob("GET_MODEL", {
  16. modelName: "song"
  17. });
  18. songModel.findOne({ _id: data.songId }, (err, song) => {
  19. WSModule.runJob("EMIT_TO_ROOMS", {
  20. rooms: [
  21. "import-album",
  22. "admin.songs",
  23. "admin.unverifiedSongs",
  24. "admin.hiddenSongs",
  25. `edit-song.${data.songId}`
  26. ],
  27. args: ["event:admin.song.updated", { data: { song, oldStatus: data.oldStatus } }]
  28. });
  29. });
  30. }
  31. });
  32. CacheModule.runJob("SUB", {
  33. channel: "song.like",
  34. cb: data => {
  35. WSModule.runJob("EMIT_TO_ROOM", {
  36. room: `song.${data.youtubeId}`,
  37. args: [
  38. "event:song.liked",
  39. {
  40. data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes }
  41. }
  42. ]
  43. });
  44. WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
  45. sockets.forEach(socket => {
  46. socket.dispatch("event:song.ratings.updated", {
  47. data: {
  48. youtubeId: data.youtubeId,
  49. liked: true,
  50. disliked: false
  51. }
  52. });
  53. });
  54. });
  55. }
  56. });
  57. CacheModule.runJob("SUB", {
  58. channel: "song.dislike",
  59. cb: data => {
  60. WSModule.runJob("EMIT_TO_ROOM", {
  61. room: `song.${data.youtubeId}`,
  62. args: [
  63. "event:song.disliked",
  64. {
  65. data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes }
  66. }
  67. ]
  68. });
  69. WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
  70. sockets.forEach(socket => {
  71. socket.dispatch("event:song.ratings.updated", {
  72. data: {
  73. youtubeId: data.youtubeId,
  74. liked: false,
  75. disliked: true
  76. }
  77. });
  78. });
  79. });
  80. }
  81. });
  82. CacheModule.runJob("SUB", {
  83. channel: "song.unlike",
  84. cb: data => {
  85. WSModule.runJob("EMIT_TO_ROOM", {
  86. room: `song.${data.youtubeId}`,
  87. args: [
  88. "event:song.unliked",
  89. {
  90. data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes }
  91. }
  92. ]
  93. });
  94. WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
  95. sockets.forEach(socket => {
  96. socket.dispatch("event:song.ratings.updated", {
  97. data: {
  98. youtubeId: data.youtubeId,
  99. liked: false,
  100. disliked: false
  101. }
  102. });
  103. });
  104. });
  105. }
  106. });
  107. CacheModule.runJob("SUB", {
  108. channel: "song.undislike",
  109. cb: data => {
  110. WSModule.runJob("EMIT_TO_ROOM", {
  111. room: `song.${data.youtubeId}`,
  112. args: [
  113. "event:song.undisliked",
  114. {
  115. data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes }
  116. }
  117. ]
  118. });
  119. WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
  120. sockets.forEach(socket => {
  121. socket.dispatch("event:song.ratings.updated", {
  122. data: {
  123. youtubeId: data.youtubeId,
  124. liked: false,
  125. disliked: false
  126. }
  127. });
  128. });
  129. });
  130. }
  131. });
  132. export default {
  133. /**
  134. * Returns the length of the songs list
  135. *
  136. * @param {object} session - the session object automatically added by the websocket
  137. * @param cb
  138. */
  139. length: isAdminRequired(async function length(session, cb) {
  140. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  141. async.waterfall(
  142. [
  143. next => {
  144. songModel.countDocuments({}, next);
  145. }
  146. ],
  147. async (err, count) => {
  148. if (err) {
  149. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  150. this.log("ERROR", "SONGS_LENGTH", `Failed to get length from songs. "${err}"`);
  151. return cb({ status: "error", message: err });
  152. }
  153. this.log("SUCCESS", "SONGS_LENGTH", `Got length from songs successfully.`);
  154. return cb({ status: "success", message: "Successfully got length of songs.", data: { length: count } });
  155. }
  156. );
  157. }),
  158. /**
  159. * Gets a set of songs
  160. *
  161. * @param {object} session - the session object automatically added by the websocket
  162. * @param set - the set number to return
  163. * @param cb
  164. */
  165. getSet: isAdminRequired(async function getSet(session, set, cb) {
  166. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  167. async.waterfall(
  168. [
  169. next => {
  170. songModel
  171. .find({})
  172. .skip(15 * (set - 1))
  173. .limit(15)
  174. .exec(next);
  175. }
  176. ],
  177. async (err, songs) => {
  178. if (err) {
  179. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  180. this.log("ERROR", "SONGS_GET_SET", `Failed to get set from songs. "${err}"`);
  181. return cb({ status: "error", message: err });
  182. }
  183. this.log("SUCCESS", "SONGS_GET_SET", `Got set from songs successfully.`);
  184. return cb({ status: "success", message: "Successfully got set of songs.", data: { songs } });
  185. }
  186. );
  187. }),
  188. /**
  189. * Updates all songs
  190. *
  191. * @param {object} session - the session object automatically added by the websocket
  192. * @param cb
  193. */
  194. updateAll: isAdminRequired(async function updateAll(session, cb) {
  195. async.waterfall(
  196. [
  197. next => {
  198. SongsModule.runJob("UPDATE_ALL_SONGS", {}, this)
  199. .then(() => {
  200. next();
  201. })
  202. .catch(err => {
  203. next(err);
  204. });
  205. }
  206. ],
  207. async err => {
  208. if (err) {
  209. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  210. this.log("ERROR", "SONGS_UPDATE_ALL", `Failed to update all songs. "${err}"`);
  211. return cb({ status: "error", message: err });
  212. }
  213. this.log("SUCCESS", "SONGS_UPDATE_ALL", `Updated all songs successfully.`);
  214. return cb({ status: "success", message: "Successfully updated all songs." });
  215. }
  216. );
  217. }),
  218. /**
  219. * Recalculates all song ratings
  220. *
  221. * @param {object} session - the session object automatically added by the websocket
  222. * @param cb
  223. */
  224. recalculateAllRatings: isAdminRequired(async function recalculateAllRatings(session, cb) {
  225. async.waterfall(
  226. [
  227. next => {
  228. SongsModule.runJob("RECALCULATE_ALL_SONG_RATINGS", {}, this)
  229. .then(() => {
  230. next();
  231. })
  232. .catch(err => {
  233. next(err);
  234. });
  235. }
  236. ],
  237. async err => {
  238. if (err) {
  239. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  240. this.log(
  241. "ERROR",
  242. "SONGS_RECALCULATE_ALL_RATINGS",
  243. `Failed to recalculate all song ratings. "${err}"`
  244. );
  245. return cb({ status: "error", message: err });
  246. }
  247. this.log("SUCCESS", "SONGS_RECALCULATE_ALL_RATINGS", `Recalculated all song ratings successfully.`);
  248. return cb({ status: "success", message: "Successfully recalculated all song ratings." });
  249. }
  250. );
  251. }),
  252. /**
  253. * Gets a song from the Musare song id
  254. *
  255. * @param {object} session - the session object automatically added by the websocket
  256. * @param {string} songId - the song id
  257. * @param {Function} cb
  258. */
  259. getSongFromSongId: isAdminRequired(function getSongFromSongId(session, songId, cb) {
  260. async.waterfall(
  261. [
  262. next => {
  263. SongsModule.runJob("GET_SONG", { songId }, this)
  264. .then(response => next(null, response.song))
  265. .catch(err => next(err));
  266. }
  267. ],
  268. async (err, song) => {
  269. if (err) {
  270. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  271. this.log("ERROR", "SONGS_GET_SONG_FROM_MUSARE_ID", `Failed to get song ${songId}. "${err}"`);
  272. return cb({ status: "error", message: err });
  273. }
  274. this.log("SUCCESS", "SONGS_GET_SONG_FROM_MUSARE_ID", `Got song ${songId} successfully.`);
  275. return cb({ status: "success", data: { song } });
  276. }
  277. );
  278. }),
  279. /**
  280. * Updates a song
  281. *
  282. * @param {object} session - the session object automatically added by the websocket
  283. * @param {string} songId - the song id
  284. * @param {object} song - the updated song object
  285. * @param {Function} cb
  286. */
  287. update: isAdminRequired(async function update(session, songId, song, cb) {
  288. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  289. let existingSong = null;
  290. async.waterfall(
  291. [
  292. next => {
  293. songModel.findOne({ _id: songId }, next);
  294. },
  295. (_existingSong, next) => {
  296. existingSong = _existingSong;
  297. songModel.updateOne({ _id: songId }, song, { runValidators: true }, next);
  298. },
  299. (res, next) => {
  300. SongsModule.runJob("UPDATE_SONG", { songId }, this)
  301. .then(song => {
  302. existingSong.genres
  303. .concat(song.genres)
  304. .filter((value, index, self) => self.indexOf(value) === index)
  305. .forEach(genre => {
  306. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
  307. .then(() => {})
  308. .catch(() => {});
  309. });
  310. next(null, song);
  311. })
  312. .catch(next);
  313. }
  314. ],
  315. async (err, song) => {
  316. if (err) {
  317. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  318. this.log("ERROR", "SONGS_UPDATE", `Failed to update song "${songId}". "${err}"`);
  319. return cb({ status: "error", message: err });
  320. }
  321. this.log("SUCCESS", "SONGS_UPDATE", `Successfully updated song "${songId}".`);
  322. return cb({
  323. status: "success",
  324. message: "Song has been successfully updated",
  325. data: { song }
  326. });
  327. }
  328. );
  329. }),
  330. // /**
  331. // * Removes a song
  332. // *
  333. // * @param session
  334. // * @param songId - the song id
  335. // * @param cb
  336. // */
  337. // remove: isAdminRequired(async function remove(session, songId, cb) {
  338. // const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  339. // let song = null;
  340. // async.waterfall(
  341. // [
  342. // next => {
  343. // songModel.findOne({ _id: songId }, next);
  344. // },
  345. // (_song, next) => {
  346. // song = _song;
  347. // songModel.deleteOne({ _id: songId }, next);
  348. // },
  349. // (res, next) => {
  350. // CacheModule.runJob("HDEL", { table: "songs", key: songId }, this)
  351. // .then(() => {
  352. // next();
  353. // })
  354. // .catch(next)
  355. // .finally(() => {
  356. // song.genres.forEach(genre => {
  357. // PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
  358. // .then(() => {})
  359. // .catch(() => {});
  360. // });
  361. // });
  362. // }
  363. // ],
  364. // async err => {
  365. // if (err) {
  366. // err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  367. // this.log("ERROR", "SONGS_REMOVE", `Failed to remove song "${songId}". "${err}"`);
  368. // return cb({ status: "error", message: err });
  369. // }
  370. // this.log("SUCCESS", "SONGS_REMOVE", `Successfully removed song "${songId}".`);
  371. // if (song.status === "verified") {
  372. // CacheModule.runJob("PUB", {
  373. // channel: "song.removedVerifiedSong",
  374. // value: songId
  375. // });
  376. // }
  377. // if (song.status === "unverified") {
  378. // CacheModule.runJob("PUB", {
  379. // channel: "song.removedUnverifiedSong",
  380. // value: songId
  381. // });
  382. // }
  383. // if (song.status === "hidden") {
  384. // CacheModule.runJob("PUB", {
  385. // channel: "song.removedHiddenSong",
  386. // value: songId
  387. // });
  388. // }
  389. // return cb({
  390. // status: "success",
  391. // message: "Song has been successfully removed"
  392. // });
  393. // }
  394. // );
  395. // }),
  396. /**
  397. * Searches through official songs
  398. *
  399. * @param {object} session - the session object automatically added by the websocket
  400. * @param {string} query - the query
  401. * @param {string} page - the page
  402. * @param {Function} cb - gets called with the result
  403. */
  404. searchOfficial: isLoginRequired(async function searchOfficial(session, query, page, cb) {
  405. async.waterfall(
  406. [
  407. next => {
  408. if ((!query && query !== "") || typeof query !== "string") next("Invalid query.");
  409. else next();
  410. },
  411. next => {
  412. SongsModule.runJob("SEARCH", {
  413. query,
  414. includeVerified: true,
  415. trimmed: true,
  416. page
  417. })
  418. .then(response => {
  419. next(null, response);
  420. })
  421. .catch(err => {
  422. next(err);
  423. });
  424. }
  425. ],
  426. async (err, data) => {
  427. if (err) {
  428. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  429. this.log("ERROR", "SONGS_SEARCH_OFFICIAL", `Searching songs failed. "${err}"`);
  430. return cb({ status: "error", message: err });
  431. }
  432. this.log("SUCCESS", "SONGS_SEARCH_OFFICIAL", "Searching songs successful.");
  433. return cb({ status: "success", data });
  434. }
  435. );
  436. }),
  437. /**
  438. * Requests a song
  439. *
  440. * @param {object} session - the session object automatically added by the websocket
  441. * @param {string} youtubeId - the youtube id of the song that gets requested
  442. * @param {string} returnSong - returns the simple song
  443. * @param {Function} cb - gets called with the result
  444. */
  445. request: isLoginRequired(async function add(session, youtubeId, returnSong, cb) {
  446. SongsModule.runJob("REQUEST_SONG", { youtubeId, userId: session.userId }, this)
  447. .then(response => {
  448. this.log(
  449. "SUCCESS",
  450. "SONGS_REQUEST",
  451. `User "${session.userId}" successfully requested song "${youtubeId}".`
  452. );
  453. return cb({
  454. status: "success",
  455. message: "Successfully requested that song",
  456. song: returnSong ? response.song : null
  457. });
  458. })
  459. .catch(async _err => {
  460. const err = await UtilsModule.runJob("GET_ERROR", { error: _err }, this);
  461. this.log(
  462. "ERROR",
  463. "SONGS_REQUEST",
  464. `Requesting song "${youtubeId}" failed for user ${session.userId}. "${err}"`
  465. );
  466. return cb({ status: "error", message: err, song: returnSong && _err.data ? _err.data.song : null });
  467. });
  468. }),
  469. /**
  470. * Hides a song
  471. *
  472. * @param {object} session - the session object automatically added by the websocket
  473. * @param {string} songId - the song id of the song that gets hidden
  474. * @param {Function} cb - gets called with the result
  475. */
  476. hide: isLoginRequired(async function add(session, songId, cb) {
  477. SongsModule.runJob("HIDE_SONG", { songId }, this)
  478. .then(() => {
  479. this.log("SUCCESS", "SONGS_HIDE", `User "${session.userId}" successfully hid song "${songId}".`);
  480. return cb({
  481. status: "success",
  482. message: "Successfully hid that song"
  483. });
  484. })
  485. .catch(async err => {
  486. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  487. this.log("ERROR", "SONGS_HIDE", `Hiding song "${songId}" failed for user ${session.userId}. "${err}"`);
  488. return cb({ status: "error", message: err });
  489. });
  490. }),
  491. /**
  492. * Unhides a song
  493. *
  494. * @param {object} session - the session object automatically added by the websocket
  495. * @param {string} songId - the song id of the song that gets hidden
  496. * @param {Function} cb - gets called with the result
  497. */
  498. unhide: isLoginRequired(async function add(session, songId, cb) {
  499. SongsModule.runJob("UNHIDE_SONG", { songId }, this)
  500. .then(() => {
  501. this.log("SUCCESS", "SONGS_UNHIDE", `User "${session.userId}" successfully unhid song "${songId}".`);
  502. return cb({
  503. status: "success",
  504. message: "Successfully unhid that song"
  505. });
  506. })
  507. .catch(async err => {
  508. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  509. this.log(
  510. "ERROR",
  511. "SONGS_UNHIDE",
  512. `Unhiding song "${songId}" failed for user ${session.userId}. "${err}"`
  513. );
  514. return cb({ status: "error", message: err });
  515. });
  516. }),
  517. /**
  518. * Verifies a song
  519. *
  520. * @param session
  521. * @param songId - the song id
  522. * @param cb
  523. */
  524. verify: isAdminRequired(async function add(session, songId, cb) {
  525. const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  526. async.waterfall(
  527. [
  528. next => {
  529. SongModel.findOne({ _id: songId }, next);
  530. },
  531. (song, next) => {
  532. if (!song) return next("This song is not in the database.");
  533. return next(null, song);
  534. },
  535. (song, next) => {
  536. const oldStatus = song.status;
  537. song.verifiedBy = session.userId;
  538. song.verifiedAt = Date.now();
  539. song.status = "verified";
  540. song.save(err => next(err, song, oldStatus));
  541. },
  542. (song, oldStatus, next) => {
  543. song.genres.forEach(genre => {
  544. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
  545. .then(() => {})
  546. .catch(() => {});
  547. });
  548. SongsModule.runJob("UPDATE_SONG", { songId: song._id, oldStatus });
  549. next(null, song, oldStatus);
  550. }
  551. ],
  552. async err => {
  553. if (err) {
  554. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  555. this.log("ERROR", "SONGS_VERIFY", `User "${session.userId}" failed to verify song. "${err}"`);
  556. return cb({ status: "error", message: err });
  557. }
  558. this.log("SUCCESS", "SONGS_VERIFY", `User "${session.userId}" successfully verified song "${songId}".`);
  559. return cb({
  560. status: "success",
  561. message: "Song has been verified successfully."
  562. });
  563. }
  564. );
  565. // TODO Check if video is in queue and Add the song to the appropriate stations
  566. }),
  567. /**
  568. * Un-verifies a song
  569. *
  570. * @param session
  571. * @param songId - the song id
  572. * @param cb
  573. */
  574. unverify: isAdminRequired(async function add(session, songId, cb) {
  575. const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  576. async.waterfall(
  577. [
  578. next => {
  579. SongModel.findOne({ _id: songId }, next);
  580. },
  581. (song, next) => {
  582. if (!song) return next("This song is not in the database.");
  583. return next(null, song);
  584. },
  585. (song, next) => {
  586. song.status = "unverified";
  587. song.save(err => {
  588. next(err, song);
  589. });
  590. },
  591. (song, next) => {
  592. song.genres.forEach(genre => {
  593. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
  594. .then(() => {})
  595. .catch(() => {});
  596. });
  597. SongsModule.runJob("UPDATE_SONG", { songId, oldStatus: "verified" });
  598. next(null);
  599. }
  600. ],
  601. async err => {
  602. if (err) {
  603. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  604. this.log("ERROR", "SONGS_UNVERIFY", `User "${session.userId}" failed to verify song. "${err}"`);
  605. return cb({ status: "error", message: err });
  606. }
  607. this.log(
  608. "SUCCESS",
  609. "SONGS_UNVERIFY",
  610. `User "${session.userId}" successfully unverified song "${songId}".`
  611. );
  612. return cb({
  613. status: "success",
  614. message: "Song has been unverified successfully."
  615. });
  616. }
  617. );
  618. // TODO Check if video is in queue and Add the song to the appropriate stations
  619. }),
  620. /**
  621. * Requests a set of songs
  622. *
  623. * @param {object} session - the session object automatically added by the websocket
  624. * @param {string} url - the url of the the YouTube playlist
  625. * @param {boolean} musicOnly - whether to only get music from the playlist
  626. * @param {Function} cb - gets called with the result
  627. */
  628. requestSet: isLoginRequired(function requestSet(session, url, musicOnly, returnSongs, cb) {
  629. async.waterfall(
  630. [
  631. next => {
  632. YouTubeModule.runJob(
  633. "GET_PLAYLIST",
  634. {
  635. url,
  636. musicOnly
  637. },
  638. this
  639. )
  640. .then(res => {
  641. next(null, res.songs);
  642. })
  643. .catch(next);
  644. },
  645. (youtubeIds, next) => {
  646. let successful = 0;
  647. let songs = {};
  648. let failed = 0;
  649. let alreadyInDatabase = 0;
  650. if (youtubeIds.length === 0) next();
  651. async.eachOfLimit(
  652. youtubeIds,
  653. 1,
  654. (youtubeId, index, next) => {
  655. WSModule.runJob(
  656. "RUN_ACTION2",
  657. {
  658. session,
  659. namespace: "songs",
  660. action: "request",
  661. args: [youtubeId, returnSongs]
  662. },
  663. this
  664. )
  665. .then(res => {
  666. if (res.status === "success") successful += 1;
  667. else failed += 1;
  668. if (res.message === "This song is already in the database.") alreadyInDatabase += 1;
  669. if (res.song) songs[index] = res.song;
  670. else songs[index] = null;
  671. })
  672. .catch(() => {
  673. failed += 1;
  674. })
  675. .finally(() => {
  676. next();
  677. });
  678. },
  679. () => {
  680. if (returnSongs)
  681. songs = Object.keys(songs)
  682. .sort()
  683. .map(key => songs[key]);
  684. next(null, { successful, failed, alreadyInDatabase, songs });
  685. }
  686. );
  687. }
  688. ],
  689. async (err, response) => {
  690. if (err) {
  691. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  692. this.log(
  693. "ERROR",
  694. "REQUEST_SET",
  695. `Importing a YouTube playlist to be requested failed for user "${session.userId}". "${err}"`
  696. );
  697. return cb({ status: "error", message: err });
  698. }
  699. this.log(
  700. "SUCCESS",
  701. "REQUEST_SET",
  702. `Successfully imported a YouTube playlist to be requested for user "${session.userId}".`
  703. );
  704. return cb({
  705. status: "success",
  706. message: `Playlist is done importing. ${response.successful} were added succesfully, ${response.failed} failed (${response.alreadyInDatabase} were already in database)`,
  707. songs: returnSongs ? response.songs : null
  708. });
  709. }
  710. );
  711. }),
  712. /**
  713. * Likes a song
  714. *
  715. * @param session
  716. * @param youtubeId - the youtube id
  717. * @param cb
  718. */
  719. like: isLoginRequired(async function like(session, youtubeId, cb) {
  720. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  721. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  722. async.waterfall(
  723. [
  724. next => songModel.findOne({ youtubeId }, next),
  725. (song, next) => {
  726. if (!song) return next("No song found with that id.");
  727. return next(null, song);
  728. },
  729. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  730. (song, user, next) => {
  731. if (!user) return next("User does not exist.");
  732. return this.module
  733. .runJob(
  734. "RUN_ACTION2",
  735. {
  736. session,
  737. namespace: "playlists",
  738. action: "addSongToPlaylist",
  739. args: [false, youtubeId, user.likedSongsPlaylist]
  740. },
  741. this
  742. )
  743. .then(res => {
  744. if (res.status === "error") {
  745. if (res.message === "That song is already in the playlist")
  746. return next("You have already liked this song.");
  747. return next("Unable to add song to the 'Liked Songs' playlist.");
  748. }
  749. return next(null, song, user.dislikedSongsPlaylist);
  750. })
  751. .catch(err => next(err));
  752. },
  753. (song, dislikedSongsPlaylist, next) => {
  754. this.module
  755. .runJob(
  756. "RUN_ACTION2",
  757. {
  758. session,
  759. namespace: "playlists",
  760. action: "removeSongFromPlaylist",
  761. args: [youtubeId, dislikedSongsPlaylist]
  762. },
  763. this
  764. )
  765. .then(res => {
  766. if (res.status === "error")
  767. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  768. return next(null, song);
  769. })
  770. .catch(err => next(err));
  771. },
  772. (song, next) => {
  773. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  774. .then(ratings => next(null, song, ratings))
  775. .catch(err => next(err));
  776. }
  777. ],
  778. async (err, song, { likes, dislikes }) => {
  779. if (err) {
  780. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  781. this.log(
  782. "ERROR",
  783. "SONGS_LIKE",
  784. `User "${session.userId}" failed to like song ${youtubeId}. "${err}"`
  785. );
  786. return cb({ status: "error", message: err });
  787. }
  788. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  789. CacheModule.runJob("PUB", {
  790. channel: "song.like",
  791. value: JSON.stringify({
  792. youtubeId,
  793. userId: session.userId,
  794. likes,
  795. dislikes
  796. })
  797. });
  798. ActivitiesModule.runJob("ADD_ACTIVITY", {
  799. userId: session.userId,
  800. type: "song__like",
  801. payload: {
  802. message: `Liked song <youtubeId>${song.title} by ${song.artists.join(", ")}</youtubeId>`,
  803. youtubeId,
  804. thumbnail: song.thumbnail
  805. }
  806. });
  807. return cb({
  808. status: "success",
  809. message: "You have successfully liked this song."
  810. });
  811. }
  812. );
  813. }),
  814. /**
  815. * Dislikes a song
  816. *
  817. * @param session
  818. * @param youtubeId - the youtube id
  819. * @param cb
  820. */
  821. dislike: isLoginRequired(async function dislike(session, youtubeId, cb) {
  822. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  823. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  824. async.waterfall(
  825. [
  826. next => {
  827. songModel.findOne({ youtubeId }, next);
  828. },
  829. (song, next) => {
  830. if (!song) return next("No song found with that id.");
  831. return next(null, song);
  832. },
  833. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  834. (song, user, next) => {
  835. if (!user) return next("User does not exist.");
  836. return this.module
  837. .runJob(
  838. "RUN_ACTION2",
  839. {
  840. session,
  841. namespace: "playlists",
  842. action: "addSongToPlaylist",
  843. args: [false, youtubeId, user.dislikedSongsPlaylist]
  844. },
  845. this
  846. )
  847. .then(res => {
  848. if (res.status === "error") {
  849. if (res.message === "That song is already in the playlist")
  850. return next("You have already disliked this song.");
  851. return next("Unable to add song to the 'Disliked Songs' playlist.");
  852. }
  853. return next(null, song, user.likedSongsPlaylist);
  854. })
  855. .catch(err => next(err));
  856. },
  857. (song, likedSongsPlaylist, next) => {
  858. this.module
  859. .runJob(
  860. "RUN_ACTION2",
  861. {
  862. session,
  863. namespace: "playlists",
  864. action: "removeSongFromPlaylist",
  865. args: [youtubeId, likedSongsPlaylist]
  866. },
  867. this
  868. )
  869. .then(res => {
  870. if (res.status === "error")
  871. return next("Unable to remove song from the 'Liked Songs' playlist.");
  872. return next(null, song);
  873. })
  874. .catch(err => next(err));
  875. },
  876. (song, next) => {
  877. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  878. .then(ratings => next(null, song, ratings))
  879. .catch(err => next(err));
  880. }
  881. ],
  882. async (err, song, { likes, dislikes }) => {
  883. if (err) {
  884. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  885. this.log(
  886. "ERROR",
  887. "SONGS_DISLIKE",
  888. `User "${session.userId}" failed to dislike song ${youtubeId}. "${err}"`
  889. );
  890. return cb({ status: "error", message: err });
  891. }
  892. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  893. CacheModule.runJob("PUB", {
  894. channel: "song.dislike",
  895. value: JSON.stringify({
  896. youtubeId,
  897. userId: session.userId,
  898. likes,
  899. dislikes
  900. })
  901. });
  902. ActivitiesModule.runJob("ADD_ACTIVITY", {
  903. userId: session.userId,
  904. type: "song__dislike",
  905. payload: {
  906. message: `Disliked song <youtubeId>${song.title} by ${song.artists.join(", ")}</youtubeId>`,
  907. youtubeId,
  908. thumbnail: song.thumbnail
  909. }
  910. });
  911. return cb({
  912. status: "success",
  913. message: "You have successfully disliked this song."
  914. });
  915. }
  916. );
  917. }),
  918. /**
  919. * Undislikes a song
  920. *
  921. * @param session
  922. * @param youtubeId - the youtube id
  923. * @param cb
  924. */
  925. undislike: isLoginRequired(async function undislike(session, youtubeId, cb) {
  926. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  927. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  928. async.waterfall(
  929. [
  930. next => {
  931. songModel.findOne({ youtubeId }, next);
  932. },
  933. (song, next) => {
  934. if (!song) return next("No song found with that id.");
  935. return next(null, song);
  936. },
  937. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  938. (song, user, next) => {
  939. if (!user) return next("User does not exist.");
  940. return this.module
  941. .runJob(
  942. "RUN_ACTION2",
  943. {
  944. session,
  945. namespace: "playlists",
  946. action: "removeSongFromPlaylist",
  947. args: [youtubeId, user.dislikedSongsPlaylist]
  948. },
  949. this
  950. )
  951. .then(res => {
  952. if (res.status === "error")
  953. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  954. return next(null, song, user.likedSongsPlaylist);
  955. })
  956. .catch(err => next(err));
  957. },
  958. (song, likedSongsPlaylist, next) => {
  959. this.module
  960. .runJob(
  961. "RUN_ACTION2",
  962. {
  963. session,
  964. namespace: "playlists",
  965. action: "removeSongFromPlaylist",
  966. args: [youtubeId, likedSongsPlaylist]
  967. },
  968. this
  969. )
  970. .then(res => {
  971. if (res.status === "error")
  972. return next("Unable to remove song from the 'Liked Songs' playlist.");
  973. return next(null, song);
  974. })
  975. .catch(err => next(err));
  976. },
  977. (song, next) => {
  978. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  979. .then(ratings => next(null, song, ratings))
  980. .catch(err => next(err));
  981. }
  982. ],
  983. async (err, song, { likes, dislikes }) => {
  984. if (err) {
  985. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  986. this.log(
  987. "ERROR",
  988. "SONGS_UNDISLIKE",
  989. `User "${session.userId}" failed to undislike song ${youtubeId}. "${err}"`
  990. );
  991. return cb({ status: "error", message: err });
  992. }
  993. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  994. CacheModule.runJob("PUB", {
  995. channel: "song.undislike",
  996. value: JSON.stringify({
  997. youtubeId,
  998. userId: session.userId,
  999. likes,
  1000. dislikes
  1001. })
  1002. });
  1003. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1004. userId: session.userId,
  1005. type: "song__undislike",
  1006. payload: {
  1007. message: `Removed <youtubeId>${song.title} by ${song.artists.join(
  1008. ", "
  1009. )}</youtubeId> from your Disliked Songs`,
  1010. youtubeId,
  1011. thumbnail: song.thumbnail
  1012. }
  1013. });
  1014. return cb({
  1015. status: "success",
  1016. message: "You have successfully undisliked this song."
  1017. });
  1018. }
  1019. );
  1020. }),
  1021. /**
  1022. * Unlikes a song
  1023. *
  1024. * @param session
  1025. * @param youtubeId - the youtube id
  1026. * @param cb
  1027. */
  1028. unlike: isLoginRequired(async function unlike(session, youtubeId, cb) {
  1029. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1030. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1031. async.waterfall(
  1032. [
  1033. next => {
  1034. songModel.findOne({ youtubeId }, next);
  1035. },
  1036. (song, next) => {
  1037. if (!song) return next("No song found with that id.");
  1038. return next(null, song);
  1039. },
  1040. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  1041. (song, user, next) => {
  1042. if (!user) return next("User does not exist.");
  1043. return this.module
  1044. .runJob(
  1045. "RUN_ACTION2",
  1046. {
  1047. session,
  1048. namespace: "playlists",
  1049. action: "removeSongFromPlaylist",
  1050. args: [youtubeId, user.dislikedSongsPlaylist]
  1051. },
  1052. this
  1053. )
  1054. .then(res => {
  1055. if (res.status === "error")
  1056. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  1057. return next(null, song, user.likedSongsPlaylist);
  1058. })
  1059. .catch(err => next(err));
  1060. },
  1061. (song, likedSongsPlaylist, next) => {
  1062. this.module
  1063. .runJob(
  1064. "RUN_ACTION2",
  1065. {
  1066. session,
  1067. namespace: "playlists",
  1068. action: "removeSongFromPlaylist",
  1069. args: [youtubeId, likedSongsPlaylist]
  1070. },
  1071. this
  1072. )
  1073. .then(res => {
  1074. if (res.status === "error")
  1075. return next("Unable to remove song from the 'Liked Songs' playlist.");
  1076. return next(null, song);
  1077. })
  1078. .catch(err => next(err));
  1079. },
  1080. (song, next) => {
  1081. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1082. .then(ratings => next(null, song, ratings))
  1083. .catch(err => next(err));
  1084. }
  1085. ],
  1086. async (err, song, { likes, dislikes }) => {
  1087. if (err) {
  1088. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1089. this.log(
  1090. "ERROR",
  1091. "SONGS_UNLIKE",
  1092. `User "${session.userId}" failed to unlike song ${youtubeId}. "${err}"`
  1093. );
  1094. return cb({ status: "error", message: err });
  1095. }
  1096. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1097. CacheModule.runJob("PUB", {
  1098. channel: "song.unlike",
  1099. value: JSON.stringify({
  1100. youtubeId,
  1101. userId: session.userId,
  1102. likes,
  1103. dislikes
  1104. })
  1105. });
  1106. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1107. userId: session.userId,
  1108. type: "song__unlike",
  1109. payload: {
  1110. message: `Removed <youtubeId>${song.title} by ${song.artists.join(
  1111. ", "
  1112. )}</youtubeId> from your Liked Songs`,
  1113. youtubeId,
  1114. thumbnail: song.thumbnail
  1115. }
  1116. });
  1117. return cb({
  1118. status: "success",
  1119. message: "You have successfully unliked this song."
  1120. });
  1121. }
  1122. );
  1123. }),
  1124. /**
  1125. * Gets song ratings
  1126. *
  1127. * @param session
  1128. * @param songId - the Musare song id
  1129. * @param cb
  1130. */
  1131. getSongRatings: isLoginRequired(async function getSongRatings(session, songId, cb) {
  1132. async.waterfall(
  1133. [
  1134. next => {
  1135. SongsModule.runJob("GET_SONG", { songId }, this)
  1136. .then(res => next(null, res.song))
  1137. .catch(next);
  1138. },
  1139. (song, next) => {
  1140. next(null, {
  1141. likes: song.likes,
  1142. dislikes: song.dislikes
  1143. });
  1144. }
  1145. ],
  1146. async (err, ratings) => {
  1147. if (err) {
  1148. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1149. this.log(
  1150. "ERROR",
  1151. "SONGS_GET_RATINGS",
  1152. `User "${session.userId}" failed to get ratings for ${songId}. "${err}"`
  1153. );
  1154. return cb({ status: "error", message: err });
  1155. }
  1156. const { likes, dislikes } = ratings;
  1157. return cb({
  1158. status: "success",
  1159. data: {
  1160. likes,
  1161. dislikes
  1162. }
  1163. });
  1164. }
  1165. );
  1166. }),
  1167. /**
  1168. * Gets user's own song ratings
  1169. *
  1170. * @param session
  1171. * @param youtubeId - the youtube id
  1172. * @param cb
  1173. */
  1174. getOwnSongRatings: isLoginRequired(async function getOwnSongRatings(session, youtubeId, cb) {
  1175. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  1176. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1177. async.waterfall(
  1178. [
  1179. next => songModel.findOne({ youtubeId }, next),
  1180. (song, next) => {
  1181. if (!song) return next("No song found with that id.");
  1182. return next(null);
  1183. },
  1184. next =>
  1185. playlistModel.findOne(
  1186. { createdBy: session.userId, displayName: "Liked Songs" },
  1187. (err, playlist) => {
  1188. if (err) return next(err);
  1189. if (!playlist) return next("'Liked Songs' playlist does not exist.");
  1190. let isLiked = false;
  1191. Object.values(playlist.songs).forEach(song => {
  1192. // song is found in 'liked songs' playlist
  1193. if (song.youtubeId === youtubeId) isLiked = true;
  1194. });
  1195. return next(null, isLiked);
  1196. }
  1197. ),
  1198. (isLiked, next) =>
  1199. playlistModel.findOne(
  1200. { createdBy: session.userId, displayName: "Disliked Songs" },
  1201. (err, playlist) => {
  1202. if (err) return next(err);
  1203. if (!playlist) return next("'Disliked Songs' playlist does not exist.");
  1204. const ratings = { isLiked, isDisliked: false };
  1205. Object.values(playlist.songs).forEach(song => {
  1206. // song is found in 'disliked songs' playlist
  1207. if (song.youtubeId === youtubeId) ratings.isDisliked = true;
  1208. });
  1209. return next(null, ratings);
  1210. }
  1211. )
  1212. ],
  1213. async (err, ratings) => {
  1214. if (err) {
  1215. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1216. this.log(
  1217. "ERROR",
  1218. "SONGS_GET_OWN_RATINGS",
  1219. `User "${session.userId}" failed to get ratings for ${youtubeId}. "${err}"`
  1220. );
  1221. return cb({ status: "error", message: err });
  1222. }
  1223. const { isLiked, isDisliked } = ratings;
  1224. return cb({
  1225. status: "success",
  1226. data: {
  1227. youtubeId,
  1228. liked: isLiked,
  1229. disliked: isDisliked
  1230. }
  1231. });
  1232. }
  1233. );
  1234. })
  1235. };