songs.js 41 KB

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