songs.js 36 KB

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