songs.js 36 KB

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