songs.js 37 KB

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