songs.js 40 KB

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