songs.js 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581
  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. * Updates all songs
  301. *
  302. * @param {object} session - the session object automatically added by the websocket
  303. * @param cb
  304. */
  305. updateAll: isAdminRequired(async function length(session, cb) {
  306. async.waterfall(
  307. [
  308. next => {
  309. SongsModule.runJob("UPDATE_ALL_SONGS", {})
  310. .then(() => {
  311. next();
  312. })
  313. .catch(err => {
  314. next(err);
  315. });
  316. }
  317. ],
  318. async err => {
  319. if (err) {
  320. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  321. this.log("ERROR", "SONGS_UPDATE_ALL", `Failed to update all songs. "${err}"`);
  322. return cb({ status: "error", message: err });
  323. }
  324. this.log("SUCCESS", "SONGS_UPDATE_ALL", `Updated all songs successfully.`);
  325. return cb({ status: "success", message: "Successfully updated all songs." });
  326. }
  327. );
  328. }),
  329. /**
  330. * Gets a song from the YouTube song id
  331. *
  332. * @param {object} session - the session object automatically added by the websocket
  333. * @param {string} youtubeId - the YouTube song id
  334. * @param {Function} cb
  335. */
  336. getSong: isAdminRequired(function getSong(session, youtubeId, cb) {
  337. async.waterfall(
  338. [
  339. next => {
  340. SongsModule.runJob("GET_SONG_FROM_YOUTUBE_ID", { youtubeId }, this)
  341. .then(song => {
  342. next(null, song);
  343. })
  344. .catch(err => {
  345. next(err);
  346. });
  347. }
  348. ],
  349. async (err, song) => {
  350. if (err) {
  351. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  352. this.log("ERROR", "SONGS_GET_SONG", `Failed to get song ${youtubeId}. "${err}"`);
  353. return cb({ status: "error", message: err });
  354. }
  355. this.log("SUCCESS", "SONGS_GET_SONG", `Got song ${youtubeId} successfully.`);
  356. return cb({ status: "success", data: { song } });
  357. }
  358. );
  359. }),
  360. /**
  361. * Gets a song from the Musare song id
  362. *
  363. * @param {object} session - the session object automatically added by the websocket
  364. * @param {string} songId - the song id
  365. * @param {Function} cb
  366. */
  367. getSongFromSongId: isAdminRequired(function getSong(session, songId, cb) {
  368. async.waterfall(
  369. [
  370. next => {
  371. SongsModule.runJob("GET_SONG", { songId }, this)
  372. .then(response => next(null, response.song))
  373. .catch(err => next(err));
  374. }
  375. ],
  376. async (err, song) => {
  377. if (err) {
  378. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  379. this.log("ERROR", "SONGS_GET_SONG_FROM_MUSARE_ID", `Failed to get song ${songId}. "${err}"`);
  380. return cb({ status: "error", message: err });
  381. }
  382. this.log("SUCCESS", "SONGS_GET_SONG_FROM_MUSARE_ID", `Got song ${songId} successfully.`);
  383. return cb({ status: "success", data: { song } });
  384. }
  385. );
  386. }),
  387. /**
  388. * Obtains basic metadata of a song in order to format an activity
  389. *
  390. * @param {object} session - the session object automatically added by the websocket
  391. * @param {string} youtubeId - the youtube song id
  392. * @param {Function} cb - callback
  393. */
  394. getSongForActivity(session, youtubeId, cb) {
  395. async.waterfall(
  396. [
  397. next => {
  398. SongsModule.runJob("GET_SONG_FROM_YOUTUBE_ID", { youtubeId }, this)
  399. .then(response => next(null, response.song))
  400. .catch(next);
  401. }
  402. ],
  403. async (err, song) => {
  404. if (err) {
  405. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  406. this.log(
  407. "ERROR",
  408. "SONGS_GET_SONG_FOR_ACTIVITY",
  409. `Failed to obtain metadata of song ${youtubeId} for activity formatting. "${err}"`
  410. );
  411. return cb({ status: "error", message: err });
  412. }
  413. if (song) {
  414. this.log(
  415. "SUCCESS",
  416. "SONGS_GET_SONG_FOR_ACTIVITY",
  417. `Obtained metadata of song ${youtubeId} for activity formatting successfully.`
  418. );
  419. return cb({
  420. status: "success",
  421. data: {
  422. title: song.title,
  423. thumbnail: song.thumbnail
  424. }
  425. });
  426. }
  427. this.log(
  428. "ERROR",
  429. "SONGS_GET_SONG_FOR_ACTIVITY",
  430. `Song ${youtubeId} does not exist so failed to obtain for activity formatting.`
  431. );
  432. return cb({ status: "error" });
  433. }
  434. );
  435. },
  436. /**
  437. * Updates a song
  438. *
  439. * @param {object} session - the session object automatically added by the websocket
  440. * @param {string} songId - the song id
  441. * @param {object} song - the updated song object
  442. * @param {Function} cb
  443. */
  444. update: isAdminRequired(async function update(session, songId, song, cb) {
  445. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  446. let existingSong = null;
  447. async.waterfall(
  448. [
  449. next => {
  450. songModel.findOne({ _id: songId }, next);
  451. },
  452. (_existingSong, next) => {
  453. existingSong = _existingSong;
  454. songModel.updateOne({ _id: songId }, song, { runValidators: true }, next);
  455. },
  456. (res, next) => {
  457. SongsModule.runJob("UPDATE_SONG", { songId }, this)
  458. .then(song => {
  459. existingSong.genres
  460. .concat(song.genres)
  461. .filter((value, index, self) => self.indexOf(value) === index)
  462. .forEach(genre => {
  463. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
  464. .then(() => {})
  465. .catch(() => {});
  466. });
  467. next(null, song);
  468. })
  469. .catch(next);
  470. }
  471. ],
  472. async (err, song) => {
  473. if (err) {
  474. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  475. this.log("ERROR", "SONGS_UPDATE", `Failed to update song "${songId}". "${err}"`);
  476. return cb({ status: "error", message: err });
  477. }
  478. this.log("SUCCESS", "SONGS_UPDATE", `Successfully updated song "${songId}".`);
  479. if (song.status === "verified") {
  480. CacheModule.runJob("PUB", {
  481. channel: "song.updatedVerifiedSong",
  482. value: song._id
  483. });
  484. } else if (song.status === "unverified") {
  485. CacheModule.runJob("PUB", {
  486. channel: "song.updatedUnverifiedSong",
  487. value: song._id
  488. });
  489. } else if (song.status === "hidden") {
  490. CacheModule.runJob("PUB", {
  491. channel: "song.updatedHiddenSong",
  492. value: song._id
  493. });
  494. }
  495. return cb({
  496. status: "success",
  497. message: "Song has been successfully updated",
  498. data: { song }
  499. });
  500. }
  501. );
  502. }),
  503. // /**
  504. // * Removes a song
  505. // *
  506. // * @param session
  507. // * @param songId - the song id
  508. // * @param cb
  509. // */
  510. // remove: isAdminRequired(async function remove(session, songId, cb) {
  511. // const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  512. // let song = null;
  513. // async.waterfall(
  514. // [
  515. // next => {
  516. // songModel.findOne({ _id: songId }, next);
  517. // },
  518. // (_song, next) => {
  519. // song = _song;
  520. // songModel.deleteOne({ _id: songId }, next);
  521. // },
  522. // (res, next) => {
  523. // // TODO Check if res gets returned from above
  524. // CacheModule.runJob("HDEL", { table: "songs", key: songId }, this)
  525. // .then(() => {
  526. // next();
  527. // })
  528. // .catch(next)
  529. // .finally(() => {
  530. // song.genres.forEach(genre => {
  531. // PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
  532. // .then(() => {})
  533. // .catch(() => {});
  534. // });
  535. // });
  536. // }
  537. // ],
  538. // async err => {
  539. // if (err) {
  540. // err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  541. // this.log("ERROR", "SONGS_REMOVE", `Failed to remove song "${songId}". "${err}"`);
  542. // return cb({ status: "error", message: err });
  543. // }
  544. // this.log("SUCCESS", "SONGS_REMOVE", `Successfully remove song "${songId}".`);
  545. // if (song.verified) {
  546. // CacheModule.runJob("PUB", {
  547. // channel: "song.removedVerifiedSong",
  548. // value: songId
  549. // });
  550. // } else {
  551. // CacheModule.runJob("PUB", {
  552. // channel: "song.removedUnverifiedSong",
  553. // value: songId
  554. // });
  555. // }
  556. // return cb({
  557. // status: "success",
  558. // message: "Song has been successfully removed"
  559. // });
  560. // }
  561. // );
  562. // }),
  563. /**
  564. * Searches through official songs
  565. *
  566. * @param {object} session - the session object automatically added by the websocket
  567. * @param {string} query - the query
  568. * @param {string} page - the page
  569. * @param {Function} cb - gets called with the result
  570. */
  571. searchOfficial: isLoginRequired(async function searchOfficial(session, query, page, cb) {
  572. async.waterfall(
  573. [
  574. next => {
  575. if ((!query && query !== "") || typeof query !== "string") next("Invalid query.");
  576. else next();
  577. },
  578. next => {
  579. SongsModule.runJob("SEARCH", {
  580. query,
  581. includeVerified: true,
  582. trimmed: true,
  583. page
  584. })
  585. .then(response => {
  586. next(null, response);
  587. })
  588. .catch(err => {
  589. next(err);
  590. });
  591. }
  592. ],
  593. async (err, data) => {
  594. if (err) {
  595. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  596. this.log("ERROR", "SONGS_SEARCH_OFFICIAL", `Searching songs failed. "${err}"`);
  597. return cb({ status: "error", message: err });
  598. }
  599. this.log("SUCCESS", "SONGS_SEARCH_OFFICIAL", "Searching songs successful.");
  600. return cb({ status: "success", data });
  601. }
  602. );
  603. }),
  604. /**
  605. * Requests a song
  606. *
  607. * @param {object} session - the session object automatically added by the websocket
  608. * @param {string} youtubeId - the youtube id of the song that gets requested
  609. * @param {Function} cb - gets called with the result
  610. */
  611. request: isLoginRequired(async function add(session, youtubeId, cb) {
  612. SongsModule.runJob("REQUEST_SONG", { youtubeId, userId: session.userId }, this)
  613. .then(() => {
  614. this.log(
  615. "SUCCESS",
  616. "SONGS_REQUEST",
  617. `User "${session.userId}" successfully requested song "${youtubeId}".`
  618. );
  619. return cb({
  620. status: "success",
  621. message: "Successfully requested that song"
  622. });
  623. })
  624. .catch(async err => {
  625. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  626. this.log(
  627. "ERROR",
  628. "SONGS_REQUEST",
  629. `Requesting song "${youtubeId}" failed for user ${session.userId}. "${err}"`
  630. );
  631. return cb({ status: "error", message: err });
  632. });
  633. }),
  634. /**
  635. * Hides a song
  636. *
  637. * @param {object} session - the session object automatically added by the websocket
  638. * @param {string} songId - the song id of the song that gets hidden
  639. * @param {Function} cb - gets called with the result
  640. */
  641. hide: isLoginRequired(async function add(session, songId, cb) {
  642. SongsModule.runJob("HIDE_SONG", { songId }, this)
  643. .then(() => {
  644. this.log("SUCCESS", "SONGS_HIDE", `User "${session.userId}" successfully hid song "${songId}".`);
  645. return cb({
  646. status: "success",
  647. message: "Successfully hid that song"
  648. });
  649. })
  650. .catch(async err => {
  651. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  652. this.log("ERROR", "SONGS_HIDE", `Hiding song "${songId}" failed for user ${session.userId}. "${err}"`);
  653. return cb({ status: "error", message: err });
  654. });
  655. }),
  656. /**
  657. * Unhides a song
  658. *
  659. * @param {object} session - the session object automatically added by the websocket
  660. * @param {string} songId - the song id of the song that gets hidden
  661. * @param {Function} cb - gets called with the result
  662. */
  663. unhide: isLoginRequired(async function add(session, songId, cb) {
  664. SongsModule.runJob("UNHIDE_SONG", { songId }, this)
  665. .then(() => {
  666. this.log("SUCCESS", "SONGS_UNHIDE", `User "${session.userId}" successfully unhid song "${songId}".`);
  667. return cb({
  668. status: "success",
  669. message: "Successfully unhid that song"
  670. });
  671. })
  672. .catch(async err => {
  673. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  674. this.log(
  675. "ERROR",
  676. "SONGS_UNHIDE",
  677. `Unhiding song "${songId}" failed for user ${session.userId}. "${err}"`
  678. );
  679. return cb({ status: "error", message: err });
  680. });
  681. }),
  682. /**
  683. * Verifies a song
  684. *
  685. * @param session
  686. * @param songId - the song id
  687. * @param cb
  688. */
  689. verify: isAdminRequired(async function add(session, songId, cb) {
  690. const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  691. async.waterfall(
  692. [
  693. next => {
  694. SongModel.findOne({ _id: songId }, next);
  695. },
  696. (song, next) => {
  697. if (!song) return next("This song is not in the database.");
  698. return next(null, song);
  699. },
  700. (song, next) => {
  701. song.acceptedBy = session.userId;
  702. song.acceptedAt = Date.now();
  703. song.status = "verified";
  704. song.save(err => {
  705. next(err, song);
  706. });
  707. },
  708. (song, next) => {
  709. song.genres.forEach(genre => {
  710. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
  711. .then(() => {})
  712. .catch(() => {});
  713. });
  714. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  715. next(null, song);
  716. }
  717. ],
  718. async (err, song) => {
  719. if (err) {
  720. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  721. this.log("ERROR", "SONGS_VERIFY", `User "${session.userId}" failed to verify song. "${err}"`);
  722. return cb({ status: "error", message: err });
  723. }
  724. this.log("SUCCESS", "SONGS_VERIFY", `User "${session.userId}" successfully verified song "${songId}".`);
  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, 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 failed = 0;
  830. let alreadyInDatabase = 0;
  831. if (youtubeIds.length === 0) next();
  832. async.eachLimit(
  833. youtubeIds,
  834. 1,
  835. (youtubeId, next) => {
  836. WSModule.runJob(
  837. "RUN_ACTION2",
  838. {
  839. session,
  840. namespace: "songs",
  841. action: "request",
  842. args: [youtubeId]
  843. },
  844. this
  845. )
  846. .then(res => {
  847. if (res.status === "success") successful += 1;
  848. else failed += 1;
  849. if (res.message === "This song is already in the database.") alreadyInDatabase += 1;
  850. })
  851. .catch(() => {
  852. failed += 1;
  853. })
  854. .finally(() => {
  855. next();
  856. });
  857. },
  858. () => {
  859. next(null, { successful, failed, alreadyInDatabase });
  860. }
  861. );
  862. }
  863. ],
  864. async (err, response) => {
  865. if (err) {
  866. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  867. this.log(
  868. "ERROR",
  869. "REQUEST_SET",
  870. `Importing a YouTube playlist to be requested failed for user "${session.userId}". "${err}"`
  871. );
  872. return cb({ status: "error", message: err });
  873. }
  874. this.log(
  875. "SUCCESS",
  876. "REQUEST_SET",
  877. `Successfully imported a YouTube playlist to be requested for user "${session.userId}".`
  878. );
  879. return cb({
  880. status: "success",
  881. message: `Playlist is done importing. ${response.successful} were added succesfully, ${response.failed} failed (${response.alreadyInDatabase} were already in database)`
  882. });
  883. }
  884. );
  885. }),
  886. // /**
  887. // * Adds a song
  888. // *
  889. // * @param session
  890. // * @param song - the song object
  891. // * @param cb
  892. // */
  893. // add: isAdminRequired(async function add(session, song, cb) {
  894. // const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  895. // async.waterfall(
  896. // [
  897. // next => {
  898. // SongModel.findOne({ youtubeId: song.youtubeId }, next);
  899. // },
  900. // (existingSong, next) => {
  901. // if (existingSong) return next("Song is already in rotation.");
  902. // return next();
  903. // },
  904. // next => {
  905. // const newSong = new SongModel(song);
  906. // newSong.acceptedBy = session.userId;
  907. // newSong.acceptedAt = Date.now();
  908. // newSong.save(next);
  909. // },
  910. // (res, next) => {
  911. // this.module
  912. // .runJob(
  913. // "RUN_ACTION2",
  914. // {
  915. // session,
  916. // namespace: "queueSongs",
  917. // action: "remove",
  918. // args: [song._id]
  919. // },
  920. // this
  921. // )
  922. // .finally(() => {
  923. // song.genres.forEach(genre => {
  924. // PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
  925. // .then(() => {})
  926. // .catch(() => {});
  927. // });
  928. // next();
  929. // });
  930. // }
  931. // ],
  932. // async err => {
  933. // if (err) {
  934. // err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  935. // this.log("ERROR", "SONGS_ADD", `User "${session.userId}" failed to add song. "${err}"`);
  936. // return cb({ status: "error", message: err });
  937. // }
  938. // this.log("SUCCESS", "SONGS_ADD", `User "${session.userId}" successfully added song "${song.youtubeId}".`);
  939. // CacheModule.runJob("PUB", {
  940. // channel: "song.added",
  941. // value: song.youtubeId
  942. // });
  943. // return cb({
  944. // status: "success",
  945. // message: "Song has been moved from the queue successfully."
  946. // });
  947. // }
  948. // );
  949. // // TODO Check if video is in queue and Add the song to the appropriate stations
  950. // }),
  951. /**
  952. * Likes a song
  953. *
  954. * @param session
  955. * @param youtubeId - the youtube id
  956. * @param cb
  957. */
  958. like: isLoginRequired(async function like(session, youtubeId, cb) {
  959. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  960. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  961. async.waterfall(
  962. [
  963. next => songModel.findOne({ youtubeId }, next),
  964. (song, next) => {
  965. if (!song) return next("No song found with that id.");
  966. return next(null, song);
  967. },
  968. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  969. (song, user, next) => {
  970. if (!user) return next("User does not exist.");
  971. return this.module
  972. .runJob(
  973. "RUN_ACTION2",
  974. {
  975. session,
  976. namespace: "playlists",
  977. action: "addSongToPlaylist",
  978. args: [false, youtubeId, user.likedSongsPlaylist]
  979. },
  980. this
  981. )
  982. .then(res => {
  983. if (res.status === "error")
  984. return next("Unable to add song to the 'Liked Songs' playlist.");
  985. return next(null, song, user.dislikedSongsPlaylist);
  986. })
  987. .catch(err => next(err));
  988. },
  989. (song, dislikedSongsPlaylist, next) => {
  990. this.module
  991. .runJob(
  992. "RUN_ACTION2",
  993. {
  994. session,
  995. namespace: "playlists",
  996. action: "removeSongFromPlaylist",
  997. args: [youtubeId, dislikedSongsPlaylist]
  998. },
  999. this
  1000. )
  1001. .then(res => {
  1002. if (res.status === "error")
  1003. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  1004. return next(null, song);
  1005. })
  1006. .catch(err => next(err));
  1007. },
  1008. (song, next) => {
  1009. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1010. .then(ratings => next(null, song, ratings))
  1011. .catch(err => next(err));
  1012. }
  1013. ],
  1014. async (err, song, { likes, dislikes }) => {
  1015. if (err) {
  1016. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1017. this.log(
  1018. "ERROR",
  1019. "SONGS_LIKE",
  1020. `User "${session.userId}" failed to like song ${youtubeId}. "${err}"`
  1021. );
  1022. return cb({ status: "error", message: err });
  1023. }
  1024. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1025. CacheModule.runJob("PUB", {
  1026. channel: "song.like",
  1027. value: JSON.stringify({
  1028. youtubeId,
  1029. userId: session.userId,
  1030. likes,
  1031. dislikes
  1032. })
  1033. });
  1034. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1035. userId: session.userId,
  1036. type: "song__like",
  1037. payload: {
  1038. message: `Liked song <youtubeId>${song.title} by ${song.artists.join(", ")}</youtubeId>`,
  1039. youtubeId,
  1040. thumbnail: song.thumbnail
  1041. }
  1042. });
  1043. return cb({
  1044. status: "success",
  1045. message: "You have successfully liked this song."
  1046. });
  1047. }
  1048. );
  1049. }),
  1050. // TODO: ALready liked/disliked etc.
  1051. /**
  1052. * Dislikes a song
  1053. *
  1054. * @param session
  1055. * @param youtubeId - the youtube id
  1056. * @param cb
  1057. */
  1058. dislike: isLoginRequired(async function dislike(session, youtubeId, cb) {
  1059. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1060. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1061. async.waterfall(
  1062. [
  1063. next => {
  1064. songModel.findOne({ youtubeId }, next);
  1065. },
  1066. (song, next) => {
  1067. if (!song) return next("No song found with that id.");
  1068. return next(null, song);
  1069. },
  1070. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  1071. (song, user, next) => {
  1072. if (!user) return next("User does not exist.");
  1073. return this.module
  1074. .runJob(
  1075. "RUN_ACTION2",
  1076. {
  1077. session,
  1078. namespace: "playlists",
  1079. action: "addSongToPlaylist",
  1080. args: [false, youtubeId, user.dislikedSongsPlaylist]
  1081. },
  1082. this
  1083. )
  1084. .then(res => {
  1085. if (res.status === "error")
  1086. return next("Unable to add song to the 'Disliked Songs' playlist.");
  1087. return next(null, song, user.likedSongsPlaylist);
  1088. })
  1089. .catch(err => next(err));
  1090. },
  1091. (song, likedSongsPlaylist, next) => {
  1092. this.module
  1093. .runJob(
  1094. "RUN_ACTION2",
  1095. {
  1096. session,
  1097. namespace: "playlists",
  1098. action: "removeSongFromPlaylist",
  1099. args: [youtubeId, likedSongsPlaylist]
  1100. },
  1101. this
  1102. )
  1103. .then(res => {
  1104. if (res.status === "error")
  1105. return next("Unable to remove song from the 'Liked Songs' playlist.");
  1106. return next(null, song);
  1107. })
  1108. .catch(err => next(err));
  1109. },
  1110. (song, next) => {
  1111. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1112. .then(ratings => next(null, song, ratings))
  1113. .catch(err => next(err));
  1114. }
  1115. ],
  1116. async (err, song, { likes, dislikes }) => {
  1117. if (err) {
  1118. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1119. this.log(
  1120. "ERROR",
  1121. "SONGS_DISLIKE",
  1122. `User "${session.userId}" failed to dislike song ${youtubeId}. "${err}"`
  1123. );
  1124. return cb({ status: "error", message: err });
  1125. }
  1126. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1127. CacheModule.runJob("PUB", {
  1128. channel: "song.dislike",
  1129. value: JSON.stringify({
  1130. youtubeId,
  1131. userId: session.userId,
  1132. likes,
  1133. dislikes
  1134. })
  1135. });
  1136. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1137. userId: session.userId,
  1138. type: "song__dislike",
  1139. payload: {
  1140. message: `Disliked song <youtubeId>${song.title} by ${song.artists.join(", ")}</youtubeId>`,
  1141. youtubeId,
  1142. thumbnail: song.thumbnail
  1143. }
  1144. });
  1145. return cb({
  1146. status: "success",
  1147. message: "You have successfully disliked this song."
  1148. });
  1149. }
  1150. );
  1151. }),
  1152. /**
  1153. * Undislikes a song
  1154. *
  1155. * @param session
  1156. * @param youtubeId - the youtube id
  1157. * @param cb
  1158. */
  1159. undislike: isLoginRequired(async function undislike(session, youtubeId, cb) {
  1160. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1161. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1162. async.waterfall(
  1163. [
  1164. next => {
  1165. songModel.findOne({ youtubeId }, next);
  1166. },
  1167. (song, next) => {
  1168. if (!song) return next("No song found with that id.");
  1169. return next(null, song);
  1170. },
  1171. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  1172. (song, user, next) => {
  1173. if (!user) return next("User does not exist.");
  1174. return this.module
  1175. .runJob(
  1176. "RUN_ACTION2",
  1177. {
  1178. session,
  1179. namespace: "playlists",
  1180. action: "removeSongFromPlaylist",
  1181. args: [youtubeId, user.dislikedSongsPlaylist]
  1182. },
  1183. this
  1184. )
  1185. .then(res => {
  1186. if (res.status === "error")
  1187. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  1188. return next(null, song, user.likedSongsPlaylist);
  1189. })
  1190. .catch(err => next(err));
  1191. },
  1192. (song, likedSongsPlaylist, next) => {
  1193. this.module
  1194. .runJob(
  1195. "RUN_ACTION2",
  1196. {
  1197. session,
  1198. namespace: "playlists",
  1199. action: "removeSongFromPlaylist",
  1200. args: [youtubeId, likedSongsPlaylist]
  1201. },
  1202. this
  1203. )
  1204. .then(res => {
  1205. if (res.status === "error")
  1206. return next("Unable to remove song from the 'Liked Songs' playlist.");
  1207. return next(null, song);
  1208. })
  1209. .catch(err => next(err));
  1210. },
  1211. (song, next) => {
  1212. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1213. .then(ratings => next(null, song, ratings))
  1214. .catch(err => next(err));
  1215. }
  1216. ],
  1217. async (err, song, { likes, dislikes }) => {
  1218. if (err) {
  1219. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1220. this.log(
  1221. "ERROR",
  1222. "SONGS_UNDISLIKE",
  1223. `User "${session.userId}" failed to undislike song ${youtubeId}. "${err}"`
  1224. );
  1225. return cb({ status: "error", message: err });
  1226. }
  1227. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1228. CacheModule.runJob("PUB", {
  1229. channel: "song.undislike",
  1230. value: JSON.stringify({
  1231. youtubeId,
  1232. userId: session.userId,
  1233. likes,
  1234. dislikes
  1235. })
  1236. });
  1237. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1238. userId: session.userId,
  1239. type: "song__undislike",
  1240. payload: {
  1241. message: `Removed <youtubeId>${song.title} by ${song.artists.join(
  1242. ", "
  1243. )}</youtubeId> from your Disliked Songs`,
  1244. youtubeId,
  1245. thumbnail: song.thumbnail
  1246. }
  1247. });
  1248. return cb({
  1249. status: "success",
  1250. message: "You have successfully undisliked this song."
  1251. });
  1252. }
  1253. );
  1254. }),
  1255. /**
  1256. * Unlikes a song
  1257. *
  1258. * @param session
  1259. * @param youtubeId - the youtube id
  1260. * @param cb
  1261. */
  1262. unlike: isLoginRequired(async function unlike(session, youtubeId, cb) {
  1263. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1264. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1265. async.waterfall(
  1266. [
  1267. next => {
  1268. songModel.findOne({ youtubeId }, next);
  1269. },
  1270. (song, next) => {
  1271. if (!song) return next("No song found with that id.");
  1272. return next(null, song);
  1273. },
  1274. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  1275. (song, user, next) => {
  1276. if (!user) return next("User does not exist.");
  1277. return this.module
  1278. .runJob(
  1279. "RUN_ACTION2",
  1280. {
  1281. session,
  1282. namespace: "playlists",
  1283. action: "removeSongFromPlaylist",
  1284. args: [youtubeId, user.dislikedSongsPlaylist]
  1285. },
  1286. this
  1287. )
  1288. .then(res => {
  1289. if (res.status === "error")
  1290. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  1291. return next(null, song, user.likedSongsPlaylist);
  1292. })
  1293. .catch(err => next(err));
  1294. },
  1295. (song, likedSongsPlaylist, next) => {
  1296. this.module
  1297. .runJob(
  1298. "RUN_ACTION2",
  1299. {
  1300. session,
  1301. namespace: "playlists",
  1302. action: "removeSongFromPlaylist",
  1303. args: [youtubeId, likedSongsPlaylist]
  1304. },
  1305. this
  1306. )
  1307. .then(res => {
  1308. if (res.status === "error")
  1309. return next("Unable to remove song from the 'Liked Songs' playlist.");
  1310. return next(null, song);
  1311. })
  1312. .catch(err => next(err));
  1313. },
  1314. (song, next) => {
  1315. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1316. .then(ratings => next(null, song, ratings))
  1317. .catch(err => next(err));
  1318. }
  1319. ],
  1320. async (err, song, { likes, dislikes }) => {
  1321. if (err) {
  1322. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1323. this.log(
  1324. "ERROR",
  1325. "SONGS_UNLIKE",
  1326. `User "${session.userId}" failed to unlike song ${youtubeId}. "${err}"`
  1327. );
  1328. return cb({ status: "error", message: err });
  1329. }
  1330. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1331. CacheModule.runJob("PUB", {
  1332. channel: "song.unlike",
  1333. value: JSON.stringify({
  1334. youtubeId,
  1335. userId: session.userId,
  1336. likes,
  1337. dislikes
  1338. })
  1339. });
  1340. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1341. userId: session.userId,
  1342. type: "song__unlike",
  1343. payload: {
  1344. message: `Removed <youtubeId>${song.title} by ${song.artists.join(
  1345. ", "
  1346. )}</youtubeId> from your Liked Songs`,
  1347. youtubeId,
  1348. thumbnail: song.thumbnail
  1349. }
  1350. });
  1351. return cb({
  1352. status: "success",
  1353. message: "You have successfully unliked this song."
  1354. });
  1355. }
  1356. );
  1357. }),
  1358. /**
  1359. * Gets user's own song ratings
  1360. *
  1361. * @param session
  1362. * @param youtubeId - the youtube id
  1363. * @param cb
  1364. */
  1365. getOwnSongRatings: isLoginRequired(async function getOwnSongRatings(session, youtubeId, cb) {
  1366. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  1367. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1368. async.waterfall(
  1369. [
  1370. next => songModel.findOne({ youtubeId }, next),
  1371. (song, next) => {
  1372. if (!song) return next("No song found with that id.");
  1373. return next(null);
  1374. },
  1375. next =>
  1376. playlistModel.findOne(
  1377. { createdBy: session.userId, displayName: "Liked Songs" },
  1378. (err, playlist) => {
  1379. if (err) return next(err);
  1380. if (!playlist) return next("'Liked Songs' playlist does not exist.");
  1381. let isLiked = false;
  1382. Object.values(playlist.songs).forEach(song => {
  1383. // song is found in 'liked songs' playlist
  1384. if (song.youtubeId === youtubeId) isLiked = true;
  1385. });
  1386. return next(null, isLiked);
  1387. }
  1388. ),
  1389. (isLiked, next) =>
  1390. playlistModel.findOne(
  1391. { createdBy: session.userId, displayName: "Disliked Songs" },
  1392. (err, playlist) => {
  1393. if (err) return next(err);
  1394. if (!playlist) return next("'Disliked Songs' playlist does not exist.");
  1395. const ratings = { isLiked, isDisliked: false };
  1396. Object.values(playlist.songs).forEach(song => {
  1397. // song is found in 'disliked songs' playlist
  1398. if (song.youtubeId === youtubeId) ratings.isDisliked = true;
  1399. });
  1400. return next(null, ratings);
  1401. }
  1402. )
  1403. ],
  1404. async (err, ratings) => {
  1405. if (err) {
  1406. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1407. this.log(
  1408. "ERROR",
  1409. "SONGS_GET_OWN_RATINGS",
  1410. `User "${session.userId}" failed to get ratings for ${youtubeId}. "${err}"`
  1411. );
  1412. return cb({ status: "error", message: err });
  1413. }
  1414. const { isLiked, isDisliked } = ratings;
  1415. return cb({
  1416. status: "success",
  1417. data: {
  1418. youtubeId,
  1419. liked: isLiked,
  1420. disliked: isDisliked
  1421. }
  1422. });
  1423. }
  1424. );
  1425. })
  1426. };