songs.js 44 KB

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