songs.js 37 KB

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