songs.js 27 KB

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