songs.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994
  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 IOModule = moduleManager.modules.io;
  7. const CacheModule = moduleManager.modules.cache;
  8. const SongsModule = moduleManager.modules.songs;
  9. const ActivitiesModule = moduleManager.modules.activities;
  10. CacheModule.runJob("SUB", {
  11. channel: "song.removed",
  12. cb: songId => {
  13. IOModule.runJob("EMIT_TO_ROOM", {
  14. room: "admin.songs",
  15. args: ["event:admin.song.removed", songId]
  16. });
  17. }
  18. });
  19. CacheModule.runJob("SUB", {
  20. channel: "song.added",
  21. cb: async songId => {
  22. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" });
  23. songModel.findOne({ _id: songId }, (err, song) => {
  24. IOModule.runJob("EMIT_TO_ROOM", {
  25. room: "admin.songs",
  26. args: ["event:admin.song.added", song]
  27. });
  28. });
  29. }
  30. });
  31. CacheModule.runJob("SUB", {
  32. channel: "song.updated",
  33. cb: async songId => {
  34. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" });
  35. songModel.findOne({ _id: songId }, (err, song) => {
  36. IOModule.runJob("EMIT_TO_ROOM", {
  37. room: "admin.songs",
  38. args: ["event:admin.song.updated", song]
  39. });
  40. });
  41. }
  42. });
  43. CacheModule.runJob("SUB", {
  44. channel: "song.like",
  45. cb: data => {
  46. IOModule.runJob("EMIT_TO_ROOM", {
  47. room: `song.${data.songId}`,
  48. args: [
  49. "event:song.like",
  50. {
  51. songId: data.songId,
  52. likes: data.likes,
  53. dislikes: data.dislikes
  54. }
  55. ]
  56. });
  57. IOModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
  58. response.sockets.forEach(socket => {
  59. socket.emit("event:song.newRatings", {
  60. songId: data.songId,
  61. liked: true,
  62. disliked: false
  63. });
  64. });
  65. });
  66. }
  67. });
  68. CacheModule.runJob("SUB", {
  69. channel: "song.dislike",
  70. cb: data => {
  71. IOModule.runJob("EMIT_TO_ROOM", {
  72. room: `song.${data.songId}`,
  73. args: [
  74. "event:song.dislike",
  75. {
  76. songId: data.songId,
  77. likes: data.likes,
  78. dislikes: data.dislikes
  79. }
  80. ]
  81. });
  82. IOModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
  83. response.sockets.forEach(socket => {
  84. socket.emit("event:song.newRatings", {
  85. songId: data.songId,
  86. liked: false,
  87. disliked: true
  88. });
  89. });
  90. });
  91. }
  92. });
  93. CacheModule.runJob("SUB", {
  94. channel: "song.unlike",
  95. cb: data => {
  96. IOModule.runJob("EMIT_TO_ROOM", {
  97. room: `song.${data.songId}`,
  98. args: [
  99. "event:song.unlike",
  100. {
  101. songId: data.songId,
  102. likes: data.likes,
  103. dislikes: data.dislikes
  104. }
  105. ]
  106. });
  107. IOModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
  108. response.sockets.forEach(socket => {
  109. socket.emit("event:song.newRatings", {
  110. songId: data.songId,
  111. liked: false,
  112. disliked: false
  113. });
  114. });
  115. });
  116. }
  117. });
  118. CacheModule.runJob("SUB", {
  119. channel: "song.undislike",
  120. cb: data => {
  121. IOModule.runJob("EMIT_TO_ROOM", {
  122. room: `song.${data.songId}`,
  123. args: [
  124. "event:song.undislike",
  125. {
  126. songId: data.songId,
  127. likes: data.likes,
  128. dislikes: data.dislikes
  129. }
  130. ]
  131. });
  132. IOModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(response => {
  133. response.sockets.forEach(socket => {
  134. socket.emit("event:song.newRatings", {
  135. songId: data.songId,
  136. liked: false,
  137. disliked: false
  138. });
  139. });
  140. });
  141. }
  142. });
  143. export default {
  144. /**
  145. * Returns the length of the songs list
  146. *
  147. * @param {object} session - the session object automatically added by socket.io
  148. * @param cb
  149. */
  150. length: isAdminRequired(async function length(session, cb) {
  151. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  152. async.waterfall(
  153. [
  154. next => {
  155. songModel.countDocuments({}, next);
  156. }
  157. ],
  158. async (err, count) => {
  159. if (err) {
  160. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  161. this.log("ERROR", "SONGS_LENGTH", `Failed to get length from songs. "${err}"`);
  162. return cb({ status: "failure", message: err });
  163. }
  164. this.log("SUCCESS", "SONGS_LENGTH", `Got length from songs successfully.`);
  165. return cb(count);
  166. }
  167. );
  168. }),
  169. /**
  170. * Gets a set of songs
  171. *
  172. * @param {object} session - the session object automatically added by socket.io
  173. * @param set - the set number to return
  174. * @param cb
  175. */
  176. getSet: isAdminRequired(async function getSet(session, set, cb) {
  177. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  178. async.waterfall(
  179. [
  180. next => {
  181. songModel
  182. .find({})
  183. .skip(15 * (set - 1))
  184. .limit(15)
  185. .exec(next);
  186. }
  187. ],
  188. async (err, songs) => {
  189. if (err) {
  190. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  191. this.log("ERROR", "SONGS_GET_SET", `Failed to get set from songs. "${err}"`);
  192. return cb({ status: "failure", message: err });
  193. }
  194. this.log("SUCCESS", "SONGS_GET_SET", `Got set from songs successfully.`);
  195. return cb(songs);
  196. }
  197. );
  198. }),
  199. /**
  200. * Gets a song from the YouTube song id
  201. *
  202. * @param {object} session - the session object automatically added by socket.io
  203. * @param {string} songId - the YouTube song id
  204. * @param {Function} cb
  205. */
  206. getSong: isAdminRequired(function getSong(session, songId, cb) {
  207. async.waterfall(
  208. [
  209. next => {
  210. SongsModule.runJob("GET_SONG_FROM_ID", { songId }, this)
  211. .then(song => {
  212. next(null, song);
  213. })
  214. .catch(err => {
  215. next(err);
  216. });
  217. }
  218. ],
  219. async (err, song) => {
  220. if (err) {
  221. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  222. this.log("ERROR", "SONGS_GET_SONG", `Failed to get song ${songId}. "${err}"`);
  223. return cb({ status: "failure", message: err });
  224. }
  225. this.log("SUCCESS", "SONGS_GET_SONG", `Got song ${songId} successfully.`);
  226. return cb({ status: "success", data: song });
  227. }
  228. );
  229. }),
  230. /**
  231. * Gets a song from the Musare song id
  232. *
  233. * @param {object} session - the session object automatically added by socket.io
  234. * @param {string} songId - the Musare song id
  235. * @param {Function} cb
  236. */
  237. getSongFromMusareId: isAdminRequired(function getSong(session, songId, cb) {
  238. async.waterfall(
  239. [
  240. next => {
  241. SongsModule.runJob("GET_SONG", { id: songId }, this)
  242. .then(response => {
  243. next(null, response.song);
  244. })
  245. .catch(err => {
  246. next(err);
  247. });
  248. }
  249. ],
  250. async (err, song) => {
  251. if (err) {
  252. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  253. this.log("ERROR", "SONGS_GET_SONG_FROM_MUSARE_ID", `Failed to get song ${songId}. "${err}"`);
  254. return cb({ status: "failure", message: err });
  255. }
  256. this.log("SUCCESS", "SONGS_GET_SONG_FROM_MUSARE_ID", `Got song ${songId} successfully.`);
  257. return cb({ status: "success", data: { song } });
  258. }
  259. );
  260. }),
  261. /**
  262. * Obtains basic metadata of a song in order to format an activity
  263. *
  264. * @param {object} session - the session object automatically added by socket.io
  265. * @param {string} songId - the song id
  266. * @param {Function} cb - callback
  267. */
  268. getSongForActivity(session, songId, cb) {
  269. async.waterfall(
  270. [
  271. next => {
  272. SongsModule.runJob("GET_SONG_FROM_ID", { songId }, this)
  273. .then(response => next(null, response.song))
  274. .catch(next);
  275. }
  276. ],
  277. async (err, song) => {
  278. if (err) {
  279. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  280. this.log(
  281. "ERROR",
  282. "SONGS_GET_SONG_FOR_ACTIVITY",
  283. `Failed to obtain metadata of song ${songId} for activity formatting. "${err}"`
  284. );
  285. return cb({ status: "failure", message: err });
  286. }
  287. if (song) {
  288. this.log(
  289. "SUCCESS",
  290. "SONGS_GET_SONG_FOR_ACTIVITY",
  291. `Obtained metadata of song ${songId} for activity formatting successfully.`
  292. );
  293. return cb({
  294. status: "success",
  295. data: {
  296. title: song.title,
  297. thumbnail: song.thumbnail
  298. }
  299. });
  300. }
  301. this.log(
  302. "ERROR",
  303. "SONGS_GET_SONG_FOR_ACTIVITY",
  304. `Song ${songId} does not exist so failed to obtain for activity formatting.`
  305. );
  306. return cb({ status: "failure" });
  307. }
  308. );
  309. },
  310. /**
  311. * Updates a song
  312. *
  313. * @param {object} session - the session object automatically added by socket.io
  314. * @param {string} songId - the song id
  315. * @param {object} song - the updated song object
  316. * @param {Function} cb
  317. */
  318. update: isAdminRequired(async function update(session, songId, song, cb) {
  319. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  320. async.waterfall(
  321. [
  322. next => {
  323. songModel.updateOne({ _id: songId }, song, { runValidators: true }, next);
  324. },
  325. (res, next) => {
  326. SongsModule.runJob("UPDATE_SONG", { songId }, this)
  327. .then(song => {
  328. next(null, song);
  329. })
  330. .catch(next);
  331. }
  332. ],
  333. async (err, song) => {
  334. if (err) {
  335. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  336. this.log("ERROR", "SONGS_UPDATE", `Failed to update song "${songId}". "${err}"`);
  337. return cb({ status: "failure", message: err });
  338. }
  339. this.log("SUCCESS", "SONGS_UPDATE", `Successfully updated song "${songId}".`);
  340. CacheModule.runJob("PUB", {
  341. channel: "song.updated",
  342. value: song.songId
  343. });
  344. return cb({
  345. status: "success",
  346. message: "Song has been successfully updated",
  347. data: song
  348. });
  349. }
  350. );
  351. }),
  352. /**
  353. * Removes a song
  354. *
  355. * @param session
  356. * @param songId - the song id
  357. * @param cb
  358. */
  359. remove: isAdminRequired(async function remove(session, songId, cb) {
  360. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  361. async.waterfall(
  362. [
  363. next => {
  364. songModel.deleteOne({ _id: songId }, next);
  365. },
  366. (res, next) => {
  367. // TODO Check if res gets returned from above
  368. CacheModule.runJob("HDEL", { table: "songs", key: songId }, this)
  369. .then(() => {
  370. next();
  371. })
  372. .catch(next);
  373. }
  374. ],
  375. async err => {
  376. if (err) {
  377. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  378. this.log("ERROR", "SONGS_UPDATE", `Failed to remove song "${songId}". "${err}"`);
  379. return cb({ status: "failure", message: err });
  380. }
  381. this.log("SUCCESS", "SONGS_UPDATE", `Successfully remove song "${songId}".`);
  382. CacheModule.runJob("PUB", { channel: "song.removed", value: songId });
  383. return cb({
  384. status: "success",
  385. message: "Song has been successfully updated"
  386. });
  387. }
  388. );
  389. }),
  390. /**
  391. * Adds a song
  392. *
  393. * @param session
  394. * @param song - the song object
  395. * @param cb
  396. */
  397. add: isAdminRequired(async function add(session, song, cb) {
  398. const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  399. async.waterfall(
  400. [
  401. next => {
  402. SongModel.findOne({ songId: song.songId }, next);
  403. },
  404. (existingSong, next) => {
  405. if (existingSong) return next("Song is already in rotation.");
  406. return next();
  407. },
  408. next => {
  409. const newSong = new SongModel(song);
  410. newSong.acceptedBy = session.userId;
  411. newSong.acceptedAt = Date.now();
  412. newSong.save(next);
  413. },
  414. (res, next) => {
  415. this.module
  416. .runJob(
  417. "RUN_ACTION2",
  418. {
  419. session,
  420. namespace: "queueSongs",
  421. action: "remove",
  422. args: [song._id]
  423. },
  424. this
  425. )
  426. .finally(() => {
  427. next();
  428. });
  429. }
  430. ],
  431. async err => {
  432. if (err) {
  433. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  434. this.log("ERROR", "SONGS_ADD", `User "${session.userId}" failed to add song. "${err}"`);
  435. return cb({ status: "failure", message: err });
  436. }
  437. this.log("SUCCESS", "SONGS_ADD", `User "${session.userId}" successfully added song "${song.songId}".`);
  438. CacheModule.runJob("PUB", {
  439. channel: "song.added",
  440. value: song.songId
  441. });
  442. return cb({
  443. status: "success",
  444. message: "Song has been moved from the queue successfully."
  445. });
  446. }
  447. );
  448. // TODO Check if video is in queue and Add the song to the appropriate stations
  449. }),
  450. /**
  451. * Likes a song
  452. *
  453. * @param session
  454. * @param musareSongId - the song id
  455. * @param cb
  456. */
  457. like: isLoginRequired(async function like(session, musareSongId, cb) {
  458. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  459. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  460. async.waterfall(
  461. [
  462. next => {
  463. songModel.findOne({ songId: musareSongId }, next);
  464. },
  465. (song, next) => {
  466. if (!song) return next("No song found with that id.");
  467. return next(null, song._id);
  468. },
  469. (songId, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, songId, user)),
  470. (songId, user, next) => {
  471. if (!user) return next("User does not exist.");
  472. return this.module
  473. .runJob(
  474. "RUN_ACTION2",
  475. {
  476. session,
  477. namespace: "playlists",
  478. action: "addSongToPlaylist",
  479. args: [false, musareSongId, user.likedSongsPlaylist]
  480. },
  481. this
  482. )
  483. .then(res => {
  484. if (res.status === "failure")
  485. return next("Unable to add song to the 'Liked Songs' playlist.");
  486. return next(null, songId, user.dislikedSongsPlaylist);
  487. })
  488. .catch(err => next(err));
  489. },
  490. (songId, dislikedSongsPlaylist, next) => {
  491. this.module
  492. .runJob(
  493. "RUN_ACTION2",
  494. {
  495. session,
  496. namespace: "playlists",
  497. action: "removeSongFromPlaylist",
  498. args: [musareSongId, dislikedSongsPlaylist]
  499. },
  500. this
  501. )
  502. .then(res => {
  503. if (res.status === "failure")
  504. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  505. return next(null, songId);
  506. })
  507. .catch(err => next(err));
  508. },
  509. (songId, next) => {
  510. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId, musareSongId })
  511. .then(ratings => next(null, songId, ratings))
  512. .catch(err => next(err));
  513. }
  514. ],
  515. async (err, songId, { likes, dislikes }) => {
  516. if (err) {
  517. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  518. this.log(
  519. "ERROR",
  520. "SONGS_LIKE",
  521. `User "${session.userId}" failed to like song ${musareSongId}. "${err}"`
  522. );
  523. return cb({ status: "failure", message: err });
  524. }
  525. SongsModule.runJob("UPDATE_SONG", { songId });
  526. CacheModule.runJob("PUB", {
  527. channel: "song.like",
  528. value: JSON.stringify({
  529. songId: musareSongId,
  530. userId: session.userId,
  531. likes,
  532. dislikes
  533. })
  534. });
  535. ActivitiesModule.runJob("ADD_ACTIVITY", {
  536. userId: session.userId,
  537. activityType: "liked_song",
  538. payload: [songId]
  539. });
  540. return cb({
  541. status: "success",
  542. message: "You have successfully liked this song."
  543. });
  544. }
  545. );
  546. }),
  547. // TODO: ALready liked/disliked etc.
  548. /**
  549. * Dislikes a song
  550. *
  551. * @param session
  552. * @param musareSongId - the song id
  553. * @param cb
  554. */
  555. dislike: isLoginRequired(async function dislike(session, musareSongId, cb) {
  556. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  557. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  558. async.waterfall(
  559. [
  560. next => {
  561. songModel.findOne({ songId: musareSongId }, next);
  562. },
  563. (song, next) => {
  564. if (!song) return next("No song found with that id.");
  565. return next(null, song._id);
  566. },
  567. (songId, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, songId, user)),
  568. (songId, user, next) => {
  569. if (!user) return next("User does not exist.");
  570. return this.module
  571. .runJob(
  572. "RUN_ACTION2",
  573. {
  574. session,
  575. namespace: "playlists",
  576. action: "addSongToPlaylist",
  577. args: [false, musareSongId, user.dislikedSongsPlaylist]
  578. },
  579. this
  580. )
  581. .then(res => {
  582. if (res.status === "failure")
  583. return next("Unable to add song to the 'Disliked Songs' playlist.");
  584. return next(null, songId, user.likedSongsPlaylist);
  585. })
  586. .catch(err => next(err));
  587. },
  588. (songId, likedSongsPlaylist, next) => {
  589. this.module
  590. .runJob(
  591. "RUN_ACTION2",
  592. {
  593. session,
  594. namespace: "playlists",
  595. action: "removeSongFromPlaylist",
  596. args: [musareSongId, likedSongsPlaylist]
  597. },
  598. this
  599. )
  600. .then(res => {
  601. if (res.status === "failure")
  602. return next("Unable to remove song from the 'Liked Songs' playlist.");
  603. return next(null, songId);
  604. })
  605. .catch(err => next(err));
  606. },
  607. (songId, next) => {
  608. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId, musareSongId })
  609. .then(ratings => next(null, songId, ratings))
  610. .catch(err => next(err));
  611. }
  612. ],
  613. async (err, songId, { likes, dislikes }) => {
  614. if (err) {
  615. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  616. this.log(
  617. "ERROR",
  618. "SONGS_DISLIKE",
  619. `User "${session.userId}" failed to dislike song ${musareSongId}. "${err}"`
  620. );
  621. return cb({ status: "failure", message: err });
  622. }
  623. SongsModule.runJob("UPDATE_SONG", { songId });
  624. CacheModule.runJob("PUB", {
  625. channel: "song.dislike",
  626. value: JSON.stringify({
  627. songId: musareSongId,
  628. userId: session.userId,
  629. likes,
  630. dislikes
  631. })
  632. });
  633. return cb({
  634. status: "success",
  635. message: "You have successfully disliked this song."
  636. });
  637. }
  638. );
  639. }),
  640. /**
  641. * Undislikes a song
  642. *
  643. * @param session
  644. * @param musareSongId - the song id
  645. * @param cb
  646. */
  647. undislike: isLoginRequired(async function undislike(session, musareSongId, cb) {
  648. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  649. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  650. async.waterfall(
  651. [
  652. next => {
  653. songModel.findOne({ songId: musareSongId }, next);
  654. },
  655. (song, next) => {
  656. if (!song) return next("No song found with that id.");
  657. return next(null, song._id);
  658. },
  659. (songId, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, songId, user)),
  660. (songId, user, next) => {
  661. if (!user) return next("User does not exist.");
  662. return this.module
  663. .runJob(
  664. "RUN_ACTION2",
  665. {
  666. session,
  667. namespace: "playlists",
  668. action: "removeSongFromPlaylist",
  669. args: [musareSongId, user.dislikedSongsPlaylist]
  670. },
  671. this
  672. )
  673. .then(res => {
  674. if (res.status === "failure")
  675. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  676. return next(null, songId, user.likedSongsPlaylist);
  677. })
  678. .catch(err => next(err));
  679. },
  680. (songId, likedSongsPlaylist, next) => {
  681. this.module
  682. .runJob(
  683. "RUN_ACTION2",
  684. {
  685. session,
  686. namespace: "playlists",
  687. action: "removeSongFromPlaylist",
  688. args: [musareSongId, likedSongsPlaylist]
  689. },
  690. this
  691. )
  692. .then(res => {
  693. if (res.status === "failure")
  694. return next("Unable to remove song from the 'Liked Songs' playlist.");
  695. return next(null, songId);
  696. })
  697. .catch(err => next(err));
  698. },
  699. (songId, next) => {
  700. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId, musareSongId })
  701. .then(ratings => next(null, songId, ratings))
  702. .catch(err => next(err));
  703. }
  704. ],
  705. async (err, songId, { likes, dislikes }) => {
  706. if (err) {
  707. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  708. this.log(
  709. "ERROR",
  710. "SONGS_UNDISLIKE",
  711. `User "${session.userId}" failed to undislike song ${musareSongId}. "${err}"`
  712. );
  713. return cb({ status: "failure", message: err });
  714. }
  715. SongsModule.runJob("UPDATE_SONG", { songId });
  716. CacheModule.runJob("PUB", {
  717. channel: "song.undislike",
  718. value: JSON.stringify({
  719. songId: musareSongId,
  720. userId: session.userId,
  721. likes,
  722. dislikes
  723. })
  724. });
  725. return cb({
  726. status: "success",
  727. message: "You have successfully undisliked this song."
  728. });
  729. }
  730. );
  731. }),
  732. /**
  733. * Unlikes a song
  734. *
  735. * @param session
  736. * @param musareSongId - the song id
  737. * @param cb
  738. */
  739. unlike: isLoginRequired(async function unlike(session, musareSongId, cb) {
  740. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  741. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  742. async.waterfall(
  743. [
  744. next => {
  745. songModel.findOne({ songId: musareSongId }, next);
  746. },
  747. (song, next) => {
  748. if (!song) return next("No song found with that id.");
  749. return next(null, song._id);
  750. },
  751. (songId, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, songId, user)),
  752. (songId, user, next) => {
  753. if (!user) return next("User does not exist.");
  754. return this.module
  755. .runJob(
  756. "RUN_ACTION2",
  757. {
  758. session,
  759. namespace: "playlists",
  760. action: "removeSongFromPlaylist",
  761. args: [musareSongId, user.dislikedSongsPlaylist]
  762. },
  763. this
  764. )
  765. .then(res => {
  766. if (res.status === "failure")
  767. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  768. return next(null, songId, user.likedSongsPlaylist);
  769. })
  770. .catch(err => next(err));
  771. },
  772. (songId, likedSongsPlaylist, next) => {
  773. this.module
  774. .runJob(
  775. "RUN_ACTION2",
  776. {
  777. session,
  778. namespace: "playlists",
  779. action: "removeSongFromPlaylist",
  780. args: [musareSongId, likedSongsPlaylist]
  781. },
  782. this
  783. )
  784. .then(res => {
  785. if (res.status === "failure")
  786. return next("Unable to remove song from the 'Liked Songs' playlist.");
  787. return next(null, songId);
  788. })
  789. .catch(err => next(err));
  790. },
  791. (songId, next) => {
  792. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId, musareSongId })
  793. .then(ratings => next(null, songId, ratings))
  794. .catch(err => next(err));
  795. }
  796. ],
  797. async (err, songId, { likes, dislikes }) => {
  798. if (err) {
  799. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  800. this.log(
  801. "ERROR",
  802. "SONGS_UNLIKE",
  803. `User "${session.userId}" failed to unlike song ${musareSongId}. "${err}"`
  804. );
  805. return cb({ status: "failure", message: err });
  806. }
  807. SongsModule.runJob("UPDATE_SONG", { songId });
  808. CacheModule.runJob("PUB", {
  809. channel: "song.unlike",
  810. value: JSON.stringify({
  811. songId: musareSongId,
  812. userId: session.userId,
  813. likes,
  814. dislikes
  815. })
  816. });
  817. return cb({
  818. status: "success",
  819. message: "You have successfully unliked this song."
  820. });
  821. }
  822. );
  823. }),
  824. /**
  825. * Gets user's own song ratings
  826. *
  827. * @param session
  828. * @param musareSongId - the song id
  829. * @param cb
  830. */
  831. getOwnSongRatings: isLoginRequired(async function getOwnSongRatings(session, musareSongId, cb) {
  832. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  833. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  834. async.waterfall(
  835. [
  836. next => {
  837. songModel.findOne({ songId: musareSongId }, next);
  838. },
  839. (song, next) => {
  840. if (!song) return next("No song found with that id.");
  841. return next(null);
  842. },
  843. next =>
  844. playlistModel.findOne(
  845. { createdBy: session.userId, displayName: "Liked Songs" },
  846. (err, playlist) => {
  847. if (err) return next(err);
  848. if (!playlist) return next("'Liked Songs' playlist does not exist.");
  849. let isLiked = false;
  850. Object.values(playlist.songs).forEach(song => {
  851. // song is found in 'liked songs' playlist
  852. if (song.songId === musareSongId) isLiked = true;
  853. });
  854. return next(null, isLiked);
  855. }
  856. ),
  857. (isLiked, next) =>
  858. playlistModel.findOne(
  859. { createdBy: session.userId, displayName: "Disliked Songs" },
  860. (err, playlist) => {
  861. if (err) return next(err);
  862. if (!playlist) return next("'Disliked Songs' playlist does not exist.");
  863. const ratings = { isLiked, isDisliked: false };
  864. Object.values(playlist.songs).forEach(song => {
  865. // song is found in 'disliked songs' playlist
  866. if (song.songId === musareSongId) ratings.isDisliked = true;
  867. });
  868. return next(null, ratings);
  869. }
  870. )
  871. ],
  872. async (err, { isLiked, isDisliked }) => {
  873. if (err) {
  874. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  875. this.log(
  876. "ERROR",
  877. "SONGS_GET_OWN_RATINGS",
  878. `User "${session.userId}" failed to get ratings for ${musareSongId}. "${err}"`
  879. );
  880. return cb({ status: "failure", message: err });
  881. }
  882. return cb({
  883. status: "success",
  884. songId: musareSongId,
  885. liked: isLiked,
  886. disliked: isDisliked
  887. });
  888. }
  889. );
  890. })
  891. };