songs.js 35 KB

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