songs.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962
  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
  201. *
  202. * @param {object} session - the session object automatically added by socket.io
  203. * @param {string} songId - the 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. * Obtains basic metadata of a song in order to format an activity
  232. *
  233. * @param {object} session - the session object automatically added by socket.io
  234. * @param {string} songId - the song id
  235. * @param {Function} cb - callback
  236. */
  237. getSongForActivity(session, songId, cb) {
  238. async.waterfall(
  239. [
  240. next => {
  241. SongsModule.runJob("GET_SONG_FROM_ID", { songId }, this)
  242. .then(response => next(null, response.song))
  243. .catch(next);
  244. }
  245. ],
  246. async (err, song) => {
  247. if (err) {
  248. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  249. this.log(
  250. "ERROR",
  251. "SONGS_GET_SONG_FOR_ACTIVITY",
  252. `Failed to obtain metadata of song ${songId} for activity formatting. "${err}"`
  253. );
  254. return cb({ status: "failure", message: err });
  255. }
  256. if (song) {
  257. this.log(
  258. "SUCCESS",
  259. "SONGS_GET_SONG_FOR_ACTIVITY",
  260. `Obtained metadata of song ${songId} for activity formatting successfully.`
  261. );
  262. return cb({
  263. status: "success",
  264. data: {
  265. title: song.title,
  266. thumbnail: song.thumbnail
  267. }
  268. });
  269. }
  270. this.log(
  271. "ERROR",
  272. "SONGS_GET_SONG_FOR_ACTIVITY",
  273. `Song ${songId} does not exist so failed to obtain for activity formatting.`
  274. );
  275. return cb({ status: "failure" });
  276. }
  277. );
  278. },
  279. /**
  280. * Updates a song
  281. *
  282. * @param {object} session - the session object automatically added by socket.io
  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. async.waterfall(
  290. [
  291. next => {
  292. songModel.updateOne({ _id: songId }, song, { runValidators: true }, next);
  293. },
  294. (res, next) => {
  295. SongsModule.runJob("UPDATE_SONG", { songId }, this)
  296. .then(song => {
  297. next(null, song);
  298. })
  299. .catch(next);
  300. }
  301. ],
  302. async (err, song) => {
  303. if (err) {
  304. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  305. this.log("ERROR", "SONGS_UPDATE", `Failed to update song "${songId}". "${err}"`);
  306. return cb({ status: "failure", message: err });
  307. }
  308. this.log("SUCCESS", "SONGS_UPDATE", `Successfully updated song "${songId}".`);
  309. CacheModule.runJob("PUB", {
  310. channel: "song.updated",
  311. value: song.songId
  312. });
  313. return cb({
  314. status: "success",
  315. message: "Song has been successfully updated",
  316. data: song
  317. });
  318. }
  319. );
  320. }),
  321. /**
  322. * Removes a song
  323. *
  324. * @param session
  325. * @param songId - the song id
  326. * @param cb
  327. */
  328. remove: isAdminRequired(async function remove(session, songId, cb) {
  329. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  330. async.waterfall(
  331. [
  332. next => {
  333. songModel.deleteOne({ _id: songId }, next);
  334. },
  335. (res, next) => {
  336. // TODO Check if res gets returned from above
  337. CacheModule.runJob("HDEL", { table: "songs", key: songId }, this)
  338. .then(() => {
  339. next();
  340. })
  341. .catch(next);
  342. }
  343. ],
  344. async err => {
  345. if (err) {
  346. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  347. this.log("ERROR", "SONGS_UPDATE", `Failed to remove song "${songId}". "${err}"`);
  348. return cb({ status: "failure", message: err });
  349. }
  350. this.log("SUCCESS", "SONGS_UPDATE", `Successfully remove song "${songId}".`);
  351. CacheModule.runJob("PUB", { channel: "song.removed", value: songId });
  352. return cb({
  353. status: "success",
  354. message: "Song has been successfully updated"
  355. });
  356. }
  357. );
  358. }),
  359. /**
  360. * Adds a song
  361. *
  362. * @param session
  363. * @param song - the song object
  364. * @param cb
  365. */
  366. add: isAdminRequired(async function add(session, song, cb) {
  367. const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  368. async.waterfall(
  369. [
  370. next => {
  371. SongModel.findOne({ songId: song.songId }, next);
  372. },
  373. (existingSong, next) => {
  374. if (existingSong) return next("Song is already in rotation.");
  375. return next();
  376. },
  377. next => {
  378. const newSong = new SongModel(song);
  379. newSong.acceptedBy = session.userId;
  380. newSong.acceptedAt = Date.now();
  381. newSong.save(next);
  382. },
  383. (res, next) => {
  384. this.module
  385. .runJob(
  386. "RUN_ACTION2",
  387. {
  388. session,
  389. namespace: "queueSongs",
  390. action: "remove",
  391. args: [song._id]
  392. },
  393. this
  394. )
  395. .finally(() => {
  396. next();
  397. });
  398. }
  399. ],
  400. async err => {
  401. if (err) {
  402. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  403. this.log("ERROR", "SONGS_ADD", `User "${session.userId}" failed to add song. "${err}"`);
  404. return cb({ status: "failure", message: err });
  405. }
  406. this.log("SUCCESS", "SONGS_ADD", `User "${session.userId}" successfully added song "${song.songId}".`);
  407. CacheModule.runJob("PUB", {
  408. channel: "song.added",
  409. value: song.songId
  410. });
  411. return cb({
  412. status: "success",
  413. message: "Song has been moved from the queue successfully."
  414. });
  415. }
  416. );
  417. // TODO Check if video is in queue and Add the song to the appropriate stations
  418. }),
  419. /**
  420. * Likes a song
  421. *
  422. * @param session
  423. * @param musareSongId - the song id
  424. * @param cb
  425. */
  426. like: isLoginRequired(async function like(session, musareSongId, cb) {
  427. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  428. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  429. async.waterfall(
  430. [
  431. next => {
  432. songModel.findOne({ songId: musareSongId }, next);
  433. },
  434. (song, next) => {
  435. if (!song) return next("No song found with that id.");
  436. return next(null, song._id);
  437. },
  438. (songId, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, songId, user)),
  439. (songId, user, next) => {
  440. if (!user) return next("User does not exist.");
  441. return this.module
  442. .runJob(
  443. "RUN_ACTION2",
  444. {
  445. session,
  446. namespace: "playlists",
  447. action: "addSongToPlaylist",
  448. args: [false, musareSongId, user.likedSongsPlaylist]
  449. },
  450. this
  451. )
  452. .then(res => {
  453. if (res.status === "failure")
  454. return next("Unable to add song to the 'Liked Songs' playlist.");
  455. return next(null, songId, user.dislikedSongsPlaylist);
  456. })
  457. .catch(err => next(err));
  458. },
  459. (songId, dislikedSongsPlaylist, next) => {
  460. this.module
  461. .runJob(
  462. "RUN_ACTION2",
  463. {
  464. session,
  465. namespace: "playlists",
  466. action: "removeSongFromPlaylist",
  467. args: [musareSongId, dislikedSongsPlaylist]
  468. },
  469. this
  470. )
  471. .then(res => {
  472. if (res.status === "failure")
  473. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  474. return next(null, songId);
  475. })
  476. .catch(err => next(err));
  477. },
  478. (songId, next) => {
  479. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId, musareSongId })
  480. .then(ratings => next(null, songId, ratings))
  481. .catch(err => next(err));
  482. }
  483. ],
  484. async (err, songId, { likes, dislikes }) => {
  485. if (err) {
  486. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  487. this.log(
  488. "ERROR",
  489. "SONGS_LIKE",
  490. `User "${session.userId}" failed to like song ${musareSongId}. "${err}"`
  491. );
  492. return cb({ status: "failure", message: err });
  493. }
  494. SongsModule.runJob("UPDATE_SONG", { songId });
  495. CacheModule.runJob("PUB", {
  496. channel: "song.like",
  497. value: JSON.stringify({
  498. songId: musareSongId,
  499. userId: session.userId,
  500. likes,
  501. dislikes
  502. })
  503. });
  504. ActivitiesModule.runJob("ADD_ACTIVITY", {
  505. userId: session.userId,
  506. activityType: "liked_song",
  507. payload: [songId]
  508. });
  509. return cb({
  510. status: "success",
  511. message: "You have successfully liked this song."
  512. });
  513. }
  514. );
  515. }),
  516. // TODO: ALready liked/disliked etc.
  517. /**
  518. * Dislikes a song
  519. *
  520. * @param session
  521. * @param musareSongId - the song id
  522. * @param cb
  523. */
  524. dislike: isLoginRequired(async function dislike(session, musareSongId, cb) {
  525. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  526. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  527. async.waterfall(
  528. [
  529. next => {
  530. songModel.findOne({ songId: musareSongId }, next);
  531. },
  532. (song, next) => {
  533. if (!song) return next("No song found with that id.");
  534. return next(null, song._id);
  535. },
  536. (songId, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, songId, user)),
  537. (songId, user, next) => {
  538. if (!user) return next("User does not exist.");
  539. return this.module
  540. .runJob(
  541. "RUN_ACTION2",
  542. {
  543. session,
  544. namespace: "playlists",
  545. action: "addSongToPlaylist",
  546. args: [false, musareSongId, user.dislikedSongsPlaylist]
  547. },
  548. this
  549. )
  550. .then(res => {
  551. if (res.status === "failure")
  552. return next("Unable to add song to the 'Disliked Songs' playlist.");
  553. return next(null, songId, user.likedSongsPlaylist);
  554. })
  555. .catch(err => next(err));
  556. },
  557. (songId, likedSongsPlaylist, next) => {
  558. this.module
  559. .runJob(
  560. "RUN_ACTION2",
  561. {
  562. session,
  563. namespace: "playlists",
  564. action: "removeSongFromPlaylist",
  565. args: [musareSongId, likedSongsPlaylist]
  566. },
  567. this
  568. )
  569. .then(res => {
  570. if (res.status === "failure")
  571. return next("Unable to remove song from the 'Liked Songs' playlist.");
  572. return next(null, songId);
  573. })
  574. .catch(err => next(err));
  575. },
  576. (songId, next) => {
  577. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId, musareSongId })
  578. .then(ratings => next(null, songId, ratings))
  579. .catch(err => next(err));
  580. }
  581. ],
  582. async (err, songId, { likes, dislikes }) => {
  583. if (err) {
  584. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  585. this.log(
  586. "ERROR",
  587. "SONGS_DISLIKE",
  588. `User "${session.userId}" failed to dislike song ${musareSongId}. "${err}"`
  589. );
  590. return cb({ status: "failure", message: err });
  591. }
  592. SongsModule.runJob("UPDATE_SONG", { songId });
  593. CacheModule.runJob("PUB", {
  594. channel: "song.dislike",
  595. value: JSON.stringify({
  596. songId: musareSongId,
  597. userId: session.userId,
  598. likes,
  599. dislikes
  600. })
  601. });
  602. return cb({
  603. status: "success",
  604. message: "You have successfully disliked this song."
  605. });
  606. }
  607. );
  608. }),
  609. /**
  610. * Undislikes a song
  611. *
  612. * @param session
  613. * @param musareSongId - the song id
  614. * @param cb
  615. */
  616. undislike: isLoginRequired(async function undislike(session, musareSongId, cb) {
  617. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  618. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  619. async.waterfall(
  620. [
  621. next => {
  622. songModel.findOne({ songId: musareSongId }, next);
  623. },
  624. (song, next) => {
  625. if (!song) return next("No song found with that id.");
  626. return next(null, song._id);
  627. },
  628. (songId, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, songId, user)),
  629. (songId, user, next) => {
  630. if (!user) return next("User does not exist.");
  631. return this.module
  632. .runJob(
  633. "RUN_ACTION2",
  634. {
  635. session,
  636. namespace: "playlists",
  637. action: "removeSongFromPlaylist",
  638. args: [musareSongId, user.dislikedSongsPlaylist]
  639. },
  640. this
  641. )
  642. .then(res => {
  643. if (res.status === "failure")
  644. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  645. return next(null, songId, user.likedSongsPlaylist);
  646. })
  647. .catch(err => next(err));
  648. },
  649. (songId, likedSongsPlaylist, next) => {
  650. this.module
  651. .runJob(
  652. "RUN_ACTION2",
  653. {
  654. session,
  655. namespace: "playlists",
  656. action: "removeSongFromPlaylist",
  657. args: [musareSongId, likedSongsPlaylist]
  658. },
  659. this
  660. )
  661. .then(res => {
  662. if (res.status === "failure")
  663. return next("Unable to remove song from the 'Liked Songs' playlist.");
  664. return next(null, songId);
  665. })
  666. .catch(err => next(err));
  667. },
  668. (songId, next) => {
  669. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId, musareSongId })
  670. .then(ratings => next(null, songId, ratings))
  671. .catch(err => next(err));
  672. }
  673. ],
  674. async (err, songId, { likes, dislikes }) => {
  675. if (err) {
  676. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  677. this.log(
  678. "ERROR",
  679. "SONGS_UNDISLIKE",
  680. `User "${session.userId}" failed to undislike song ${musareSongId}. "${err}"`
  681. );
  682. return cb({ status: "failure", message: err });
  683. }
  684. SongsModule.runJob("UPDATE_SONG", { songId });
  685. CacheModule.runJob("PUB", {
  686. channel: "song.undislike",
  687. value: JSON.stringify({
  688. songId: musareSongId,
  689. userId: session.userId,
  690. likes,
  691. dislikes
  692. })
  693. });
  694. return cb({
  695. status: "success",
  696. message: "You have successfully undisliked this song."
  697. });
  698. }
  699. );
  700. }),
  701. /**
  702. * Unlikes a song
  703. *
  704. * @param session
  705. * @param musareSongId - the song id
  706. * @param cb
  707. */
  708. unlike: isLoginRequired(async function unlike(session, musareSongId, cb) {
  709. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  710. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  711. async.waterfall(
  712. [
  713. next => {
  714. songModel.findOne({ songId: musareSongId }, next);
  715. },
  716. (song, next) => {
  717. if (!song) return next("No song found with that id.");
  718. return next(null, song._id);
  719. },
  720. (songId, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, songId, user)),
  721. (songId, user, next) => {
  722. if (!user) return next("User does not exist.");
  723. return this.module
  724. .runJob(
  725. "RUN_ACTION2",
  726. {
  727. session,
  728. namespace: "playlists",
  729. action: "removeSongFromPlaylist",
  730. args: [musareSongId, user.dislikedSongsPlaylist]
  731. },
  732. this
  733. )
  734. .then(res => {
  735. if (res.status === "failure")
  736. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  737. return next(null, songId, user.likedSongsPlaylist);
  738. })
  739. .catch(err => next(err));
  740. },
  741. (songId, likedSongsPlaylist, next) => {
  742. this.module
  743. .runJob(
  744. "RUN_ACTION2",
  745. {
  746. session,
  747. namespace: "playlists",
  748. action: "removeSongFromPlaylist",
  749. args: [musareSongId, likedSongsPlaylist]
  750. },
  751. this
  752. )
  753. .then(res => {
  754. if (res.status === "failure")
  755. return next("Unable to remove song from the 'Liked Songs' playlist.");
  756. return next(null, songId);
  757. })
  758. .catch(err => next(err));
  759. },
  760. (songId, next) => {
  761. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId, musareSongId })
  762. .then(ratings => next(null, songId, ratings))
  763. .catch(err => next(err));
  764. }
  765. ],
  766. async (err, songId, { likes, dislikes }) => {
  767. if (err) {
  768. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  769. this.log(
  770. "ERROR",
  771. "SONGS_UNLIKE",
  772. `User "${session.userId}" failed to unlike song ${musareSongId}. "${err}"`
  773. );
  774. return cb({ status: "failure", message: err });
  775. }
  776. SongsModule.runJob("UPDATE_SONG", { songId });
  777. CacheModule.runJob("PUB", {
  778. channel: "song.unlike",
  779. value: JSON.stringify({
  780. songId: musareSongId,
  781. userId: session.userId,
  782. likes,
  783. dislikes
  784. })
  785. });
  786. return cb({
  787. status: "success",
  788. message: "You have successfully unliked this song."
  789. });
  790. }
  791. );
  792. }),
  793. /**
  794. * Gets user's own song ratings
  795. *
  796. * @param session
  797. * @param musareSongId - the song id
  798. * @param cb
  799. */
  800. getOwnSongRatings: isLoginRequired(async function getOwnSongRatings(session, musareSongId, cb) {
  801. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  802. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  803. async.waterfall(
  804. [
  805. next => {
  806. songModel.findOne({ songId: musareSongId }, next);
  807. },
  808. (song, next) => {
  809. if (!song) return next("No song found with that id.");
  810. return next(null);
  811. },
  812. next =>
  813. playlistModel.findOne(
  814. { createdBy: session.userId, displayName: "Liked Songs" },
  815. (err, playlist) => {
  816. if (err) return next(err);
  817. if (!playlist) return next("'Liked Songs' playlist does not exist.");
  818. let isLiked = false;
  819. Object.values(playlist.songs).forEach(song => {
  820. // song is found in 'liked songs' playlist
  821. if (song.songId === musareSongId) isLiked = true;
  822. });
  823. return next(null, isLiked);
  824. }
  825. ),
  826. (isLiked, next) =>
  827. playlistModel.findOne(
  828. { createdBy: session.userId, displayName: "Disliked Songs" },
  829. (err, playlist) => {
  830. if (err) return next(err);
  831. if (!playlist) return next("'Disliked Songs' playlist does not exist.");
  832. const ratings = { isLiked, isDisliked: false };
  833. Object.values(playlist.songs).forEach(song => {
  834. // song is found in 'disliked songs' playlist
  835. if (song.songId === musareSongId) ratings.isDisliked = true;
  836. });
  837. return next(null, ratings);
  838. }
  839. )
  840. ],
  841. async (err, { isLiked, isDisliked }) => {
  842. if (err) {
  843. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  844. this.log(
  845. "ERROR",
  846. "SONGS_GET_OWN_RATINGS",
  847. `User "${session.userId}" failed to get ratings for ${musareSongId}. "${err}"`
  848. );
  849. return cb({ status: "failure", message: err });
  850. }
  851. return cb({
  852. status: "success",
  853. songId: musareSongId,
  854. liked: isLiked,
  855. disliked: isDisliked
  856. });
  857. }
  858. );
  859. })
  860. };