songs.js 27 KB

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