songs.js 40 KB

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