songs.js 40 KB

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