songs.js 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595
  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 {string} returnSong - returns the simple song
  604. * @param {Function} cb - gets called with the result
  605. */
  606. request: isLoginRequired(async function add(session, youtubeId, returnSong, cb) {
  607. SongsModule.runJob("REQUEST_SONG", { youtubeId, userId: session.userId }, this)
  608. .then(response => {
  609. this.log(
  610. "SUCCESS",
  611. "SONGS_REQUEST",
  612. `User "${session.userId}" successfully requested song "${youtubeId}".`
  613. );
  614. return cb({
  615. status: "success",
  616. message: "Successfully requested that song",
  617. song: returnSong ? response.song : null
  618. });
  619. })
  620. .catch(async _err => {
  621. const err = await UtilsModule.runJob("GET_ERROR", { error: _err }, this);
  622. this.log(
  623. "ERROR",
  624. "SONGS_REQUEST",
  625. `Requesting song "${youtubeId}" failed for user ${session.userId}. "${err}"`
  626. );
  627. return cb({ status: "error", message: err, song: returnSong && _err.data ? _err.data.song : null });
  628. });
  629. }),
  630. /**
  631. * Hides a song
  632. *
  633. * @param {object} session - the session object automatically added by the websocket
  634. * @param {string} songId - the song id of the song that gets hidden
  635. * @param {Function} cb - gets called with the result
  636. */
  637. hide: isLoginRequired(async function add(session, songId, cb) {
  638. SongsModule.runJob("HIDE_SONG", { songId }, this)
  639. .then(() => {
  640. this.log("SUCCESS", "SONGS_HIDE", `User "${session.userId}" successfully hid song "${songId}".`);
  641. return cb({
  642. status: "success",
  643. message: "Successfully hid that song"
  644. });
  645. })
  646. .catch(async err => {
  647. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  648. this.log("ERROR", "SONGS_HIDE", `Hiding song "${songId}" failed for user ${session.userId}. "${err}"`);
  649. return cb({ status: "error", message: err });
  650. });
  651. }),
  652. /**
  653. * Unhides a song
  654. *
  655. * @param {object} session - the session object automatically added by the websocket
  656. * @param {string} songId - the song id of the song that gets hidden
  657. * @param {Function} cb - gets called with the result
  658. */
  659. unhide: isLoginRequired(async function add(session, songId, cb) {
  660. SongsModule.runJob("UNHIDE_SONG", { songId }, this)
  661. .then(() => {
  662. this.log("SUCCESS", "SONGS_UNHIDE", `User "${session.userId}" successfully unhid song "${songId}".`);
  663. return cb({
  664. status: "success",
  665. message: "Successfully unhid that song"
  666. });
  667. })
  668. .catch(async err => {
  669. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  670. this.log(
  671. "ERROR",
  672. "SONGS_UNHIDE",
  673. `Unhiding song "${songId}" failed for user ${session.userId}. "${err}"`
  674. );
  675. return cb({ status: "error", message: err });
  676. });
  677. }),
  678. /**
  679. * Verifies a song
  680. *
  681. * @param session
  682. * @param songId - the song id
  683. * @param cb
  684. */
  685. verify: isAdminRequired(async function add(session, songId, cb) {
  686. const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  687. async.waterfall(
  688. [
  689. next => {
  690. SongModel.findOne({ _id: songId }, next);
  691. },
  692. (song, next) => {
  693. if (!song) return next("This song is not in the database.");
  694. return next(null, song);
  695. },
  696. (song, next) => {
  697. song.verifiedBy = session.userId;
  698. song.verifiedAt = Date.now();
  699. song.status = "verified";
  700. song.save(err => {
  701. next(err, song);
  702. });
  703. },
  704. (song, next) => {
  705. song.genres.forEach(genre => {
  706. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
  707. .then(() => {})
  708. .catch(() => {});
  709. });
  710. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  711. next(null, song);
  712. }
  713. ],
  714. async (err, song) => {
  715. if (err) {
  716. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  717. this.log("ERROR", "SONGS_VERIFY", `User "${session.userId}" failed to verify song. "${err}"`);
  718. return cb({ status: "error", message: err });
  719. }
  720. this.log("SUCCESS", "SONGS_VERIFY", `User "${session.userId}" successfully verified song "${songId}".`);
  721. CacheModule.runJob("PUB", {
  722. channel: "song.newVerifiedSong",
  723. value: song._id
  724. });
  725. CacheModule.runJob("PUB", {
  726. channel: "song.removedUnverifiedSong",
  727. value: song._id
  728. });
  729. return cb({
  730. status: "success",
  731. message: "Song has been verified successfully."
  732. });
  733. }
  734. );
  735. // TODO Check if video is in queue and Add the song to the appropriate stations
  736. }),
  737. /**
  738. * Un-verifies a song
  739. *
  740. * @param session
  741. * @param songId - the song id
  742. * @param cb
  743. */
  744. unverify: isAdminRequired(async function add(session, songId, cb) {
  745. const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  746. async.waterfall(
  747. [
  748. next => {
  749. SongModel.findOne({ _id: songId }, next);
  750. },
  751. (song, next) => {
  752. if (!song) return next("This song is not in the database.");
  753. return next(null, song);
  754. },
  755. (song, next) => {
  756. song.status = "unverified";
  757. song.save(err => {
  758. next(err, song);
  759. });
  760. },
  761. (song, next) => {
  762. song.genres.forEach(genre => {
  763. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
  764. .then(() => {})
  765. .catch(() => {});
  766. });
  767. SongsModule.runJob("UPDATE_SONG", { songId });
  768. next(null);
  769. }
  770. ],
  771. async err => {
  772. if (err) {
  773. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  774. this.log("ERROR", "SONGS_UNVERIFY", `User "${session.userId}" failed to verify song. "${err}"`);
  775. return cb({ status: "error", message: err });
  776. }
  777. this.log(
  778. "SUCCESS",
  779. "SONGS_UNVERIFY",
  780. `User "${session.userId}" successfully unverified song "${songId}".`
  781. );
  782. CacheModule.runJob("PUB", {
  783. channel: "song.newUnverifiedSong",
  784. value: songId
  785. });
  786. CacheModule.runJob("PUB", {
  787. channel: "song.removedVerifiedSong",
  788. value: songId
  789. });
  790. return cb({
  791. status: "success",
  792. message: "Song has been unverified successfully."
  793. });
  794. }
  795. );
  796. // TODO Check if video is in queue and Add the song to the appropriate stations
  797. }),
  798. /**
  799. * Requests a set of songs
  800. *
  801. * @param {object} session - the session object automatically added by the websocket
  802. * @param {string} url - the url of the the YouTube playlist
  803. * @param {boolean} musicOnly - whether to only get music from the playlist
  804. * @param {Function} cb - gets called with the result
  805. */
  806. requestSet: isLoginRequired(function requestSet(session, url, musicOnly, returnSongs, cb) {
  807. async.waterfall(
  808. [
  809. next => {
  810. YouTubeModule.runJob(
  811. "GET_PLAYLIST",
  812. {
  813. url,
  814. musicOnly
  815. },
  816. this
  817. )
  818. .then(res => {
  819. next(null, res.songs);
  820. })
  821. .catch(next);
  822. },
  823. (youtubeIds, next) => {
  824. let successful = 0;
  825. let songs = {};
  826. let failed = 0;
  827. let alreadyInDatabase = 0;
  828. if (youtubeIds.length === 0) next();
  829. async.eachOfLimit(
  830. youtubeIds,
  831. 1,
  832. (youtubeId, index, next) => {
  833. WSModule.runJob(
  834. "RUN_ACTION2",
  835. {
  836. session,
  837. namespace: "songs",
  838. action: "request",
  839. args: [youtubeId, returnSongs]
  840. },
  841. this
  842. )
  843. .then(res => {
  844. if (res.status === "success") successful += 1;
  845. else failed += 1;
  846. if (res.message === "This song is already in the database.") alreadyInDatabase += 1;
  847. if (res.song) songs[index] = res.song;
  848. else songs[index] = null;
  849. })
  850. .catch(() => {
  851. failed += 1;
  852. })
  853. .finally(() => {
  854. next();
  855. });
  856. },
  857. () => {
  858. if (returnSongs)
  859. songs = Object.keys(songs)
  860. .sort()
  861. .map(key => songs[key]);
  862. next(null, { successful, failed, alreadyInDatabase, songs });
  863. }
  864. );
  865. }
  866. ],
  867. async (err, response) => {
  868. if (err) {
  869. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  870. this.log(
  871. "ERROR",
  872. "REQUEST_SET",
  873. `Importing a YouTube playlist to be requested failed for user "${session.userId}". "${err}"`
  874. );
  875. return cb({ status: "error", message: err });
  876. }
  877. this.log(
  878. "SUCCESS",
  879. "REQUEST_SET",
  880. `Successfully imported a YouTube playlist to be requested for user "${session.userId}".`
  881. );
  882. return cb({
  883. status: "success",
  884. message: `Playlist is done importing. ${response.successful} were added succesfully, ${response.failed} failed (${response.alreadyInDatabase} were already in database)`,
  885. songs: returnSongs ? response.songs : null
  886. });
  887. }
  888. );
  889. }),
  890. // /**
  891. // * Adds a song
  892. // *
  893. // * @param session
  894. // * @param song - the song object
  895. // * @param cb
  896. // */
  897. // add: isAdminRequired(async function add(session, song, cb) {
  898. // const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  899. // async.waterfall(
  900. // [
  901. // next => {
  902. // SongModel.findOne({ youtubeId: song.youtubeId }, next);
  903. // },
  904. // (existingSong, next) => {
  905. // if (existingSong) return next("Song is already in rotation.");
  906. // return next();
  907. // },
  908. // next => {
  909. // const newSong = new SongModel(song);
  910. // newSong.verifiedBy = session.userId;
  911. // newSong.verifiedAt = Date.now();
  912. // newSong.save(next);
  913. // },
  914. // (res, next) => {
  915. // this.module
  916. // .runJob(
  917. // "RUN_ACTION2",
  918. // {
  919. // session,
  920. // namespace: "queueSongs",
  921. // action: "remove",
  922. // args: [song._id]
  923. // },
  924. // this
  925. // )
  926. // .finally(() => {
  927. // song.genres.forEach(genre => {
  928. // PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
  929. // .then(() => {})
  930. // .catch(() => {});
  931. // });
  932. // next();
  933. // });
  934. // }
  935. // ],
  936. // async err => {
  937. // if (err) {
  938. // err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  939. // this.log("ERROR", "SONGS_ADD", `User "${session.userId}" failed to add song. "${err}"`);
  940. // return cb({ status: "error", message: err });
  941. // }
  942. // this.log("SUCCESS", "SONGS_ADD", `User "${session.userId}" successfully added song "${song.youtubeId}".`);
  943. // CacheModule.runJob("PUB", {
  944. // channel: "song.added",
  945. // value: song.youtubeId
  946. // });
  947. // return cb({
  948. // status: "success",
  949. // message: "Song has been moved from the queue successfully."
  950. // });
  951. // }
  952. // );
  953. // // TODO Check if video is in queue and Add the song to the appropriate stations
  954. // }),
  955. /**
  956. * Likes a song
  957. *
  958. * @param session
  959. * @param youtubeId - the youtube id
  960. * @param cb
  961. */
  962. like: isLoginRequired(async function like(session, youtubeId, cb) {
  963. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  964. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  965. async.waterfall(
  966. [
  967. next => songModel.findOne({ youtubeId }, next),
  968. (song, next) => {
  969. if (!song) return next("No song found with that id.");
  970. return next(null, song);
  971. },
  972. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  973. (song, user, next) => {
  974. if (!user) return next("User does not exist.");
  975. return this.module
  976. .runJob(
  977. "RUN_ACTION2",
  978. {
  979. session,
  980. namespace: "playlists",
  981. action: "addSongToPlaylist",
  982. args: [false, youtubeId, user.likedSongsPlaylist]
  983. },
  984. this
  985. )
  986. .then(res => {
  987. if (res.status === "error") {
  988. if (res.message === "That song is already in the playlist")
  989. return next("You have already liked this song.");
  990. return next("Unable to add song to the 'Liked Songs' playlist.");
  991. }
  992. return next(null, song, user.dislikedSongsPlaylist);
  993. })
  994. .catch(err => next(err));
  995. },
  996. (song, dislikedSongsPlaylist, next) => {
  997. this.module
  998. .runJob(
  999. "RUN_ACTION2",
  1000. {
  1001. session,
  1002. namespace: "playlists",
  1003. action: "removeSongFromPlaylist",
  1004. args: [youtubeId, dislikedSongsPlaylist]
  1005. },
  1006. this
  1007. )
  1008. .then(res => {
  1009. if (res.status === "error")
  1010. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  1011. return next(null, song);
  1012. })
  1013. .catch(err => next(err));
  1014. },
  1015. (song, next) => {
  1016. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1017. .then(ratings => next(null, song, ratings))
  1018. .catch(err => next(err));
  1019. }
  1020. ],
  1021. async (err, song, { likes, dislikes }) => {
  1022. if (err) {
  1023. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1024. this.log(
  1025. "ERROR",
  1026. "SONGS_LIKE",
  1027. `User "${session.userId}" failed to like song ${youtubeId}. "${err}"`
  1028. );
  1029. return cb({ status: "error", message: err });
  1030. }
  1031. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1032. CacheModule.runJob("PUB", {
  1033. channel: "song.like",
  1034. value: JSON.stringify({
  1035. youtubeId,
  1036. userId: session.userId,
  1037. likes,
  1038. dislikes
  1039. })
  1040. });
  1041. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1042. userId: session.userId,
  1043. type: "song__like",
  1044. payload: {
  1045. message: `Liked song <youtubeId>${song.title} by ${song.artists.join(", ")}</youtubeId>`,
  1046. youtubeId,
  1047. thumbnail: song.thumbnail
  1048. }
  1049. });
  1050. return cb({
  1051. status: "success",
  1052. message: "You have successfully liked this song."
  1053. });
  1054. }
  1055. );
  1056. }),
  1057. /**
  1058. * Dislikes a song
  1059. *
  1060. * @param session
  1061. * @param youtubeId - the youtube id
  1062. * @param cb
  1063. */
  1064. dislike: isLoginRequired(async function dislike(session, youtubeId, cb) {
  1065. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1066. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1067. async.waterfall(
  1068. [
  1069. next => {
  1070. songModel.findOne({ youtubeId }, next);
  1071. },
  1072. (song, next) => {
  1073. if (!song) return next("No song found with that id.");
  1074. return next(null, song);
  1075. },
  1076. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  1077. (song, user, next) => {
  1078. if (!user) return next("User does not exist.");
  1079. return this.module
  1080. .runJob(
  1081. "RUN_ACTION2",
  1082. {
  1083. session,
  1084. namespace: "playlists",
  1085. action: "addSongToPlaylist",
  1086. args: [false, youtubeId, user.dislikedSongsPlaylist]
  1087. },
  1088. this
  1089. )
  1090. .then(res => {
  1091. if (res.status === "error") {
  1092. if (res.message === "That song is already in the playlist")
  1093. return next("You have already disliked this song.");
  1094. return next("Unable to add song to the 'Disliked Songs' playlist.");
  1095. }
  1096. return next(null, song, user.likedSongsPlaylist);
  1097. })
  1098. .catch(err => next(err));
  1099. },
  1100. (song, likedSongsPlaylist, next) => {
  1101. this.module
  1102. .runJob(
  1103. "RUN_ACTION2",
  1104. {
  1105. session,
  1106. namespace: "playlists",
  1107. action: "removeSongFromPlaylist",
  1108. args: [youtubeId, likedSongsPlaylist]
  1109. },
  1110. this
  1111. )
  1112. .then(res => {
  1113. if (res.status === "error")
  1114. return next("Unable to remove song from the 'Liked Songs' playlist.");
  1115. return next(null, song);
  1116. })
  1117. .catch(err => next(err));
  1118. },
  1119. (song, next) => {
  1120. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1121. .then(ratings => next(null, song, ratings))
  1122. .catch(err => next(err));
  1123. }
  1124. ],
  1125. async (err, song, { likes, dislikes }) => {
  1126. if (err) {
  1127. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1128. this.log(
  1129. "ERROR",
  1130. "SONGS_DISLIKE",
  1131. `User "${session.userId}" failed to dislike song ${youtubeId}. "${err}"`
  1132. );
  1133. return cb({ status: "error", message: err });
  1134. }
  1135. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1136. CacheModule.runJob("PUB", {
  1137. channel: "song.dislike",
  1138. value: JSON.stringify({
  1139. youtubeId,
  1140. userId: session.userId,
  1141. likes,
  1142. dislikes
  1143. })
  1144. });
  1145. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1146. userId: session.userId,
  1147. type: "song__dislike",
  1148. payload: {
  1149. message: `Disliked song <youtubeId>${song.title} by ${song.artists.join(", ")}</youtubeId>`,
  1150. youtubeId,
  1151. thumbnail: song.thumbnail
  1152. }
  1153. });
  1154. return cb({
  1155. status: "success",
  1156. message: "You have successfully disliked this song."
  1157. });
  1158. }
  1159. );
  1160. }),
  1161. /**
  1162. * Undislikes a song
  1163. *
  1164. * @param session
  1165. * @param youtubeId - the youtube id
  1166. * @param cb
  1167. */
  1168. undislike: isLoginRequired(async function undislike(session, youtubeId, cb) {
  1169. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1170. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1171. async.waterfall(
  1172. [
  1173. next => {
  1174. songModel.findOne({ youtubeId }, next);
  1175. },
  1176. (song, next) => {
  1177. if (!song) return next("No song found with that id.");
  1178. return next(null, song);
  1179. },
  1180. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  1181. (song, user, next) => {
  1182. if (!user) return next("User does not exist.");
  1183. return this.module
  1184. .runJob(
  1185. "RUN_ACTION2",
  1186. {
  1187. session,
  1188. namespace: "playlists",
  1189. action: "removeSongFromPlaylist",
  1190. args: [youtubeId, user.dislikedSongsPlaylist]
  1191. },
  1192. this
  1193. )
  1194. .then(res => {
  1195. if (res.status === "error")
  1196. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  1197. return next(null, song, user.likedSongsPlaylist);
  1198. })
  1199. .catch(err => next(err));
  1200. },
  1201. (song, likedSongsPlaylist, next) => {
  1202. this.module
  1203. .runJob(
  1204. "RUN_ACTION2",
  1205. {
  1206. session,
  1207. namespace: "playlists",
  1208. action: "removeSongFromPlaylist",
  1209. args: [youtubeId, likedSongsPlaylist]
  1210. },
  1211. this
  1212. )
  1213. .then(res => {
  1214. if (res.status === "error")
  1215. return next("Unable to remove song from the 'Liked Songs' playlist.");
  1216. return next(null, song);
  1217. })
  1218. .catch(err => next(err));
  1219. },
  1220. (song, next) => {
  1221. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1222. .then(ratings => next(null, song, ratings))
  1223. .catch(err => next(err));
  1224. }
  1225. ],
  1226. async (err, song, { likes, dislikes }) => {
  1227. if (err) {
  1228. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1229. this.log(
  1230. "ERROR",
  1231. "SONGS_UNDISLIKE",
  1232. `User "${session.userId}" failed to undislike song ${youtubeId}. "${err}"`
  1233. );
  1234. return cb({ status: "error", message: err });
  1235. }
  1236. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1237. CacheModule.runJob("PUB", {
  1238. channel: "song.undislike",
  1239. value: JSON.stringify({
  1240. youtubeId,
  1241. userId: session.userId,
  1242. likes,
  1243. dislikes
  1244. })
  1245. });
  1246. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1247. userId: session.userId,
  1248. type: "song__undislike",
  1249. payload: {
  1250. message: `Removed <youtubeId>${song.title} by ${song.artists.join(
  1251. ", "
  1252. )}</youtubeId> from your Disliked Songs`,
  1253. youtubeId,
  1254. thumbnail: song.thumbnail
  1255. }
  1256. });
  1257. return cb({
  1258. status: "success",
  1259. message: "You have successfully undisliked this song."
  1260. });
  1261. }
  1262. );
  1263. }),
  1264. /**
  1265. * Unlikes a song
  1266. *
  1267. * @param session
  1268. * @param youtubeId - the youtube id
  1269. * @param cb
  1270. */
  1271. unlike: isLoginRequired(async function unlike(session, youtubeId, cb) {
  1272. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1273. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1274. async.waterfall(
  1275. [
  1276. next => {
  1277. songModel.findOne({ youtubeId }, next);
  1278. },
  1279. (song, next) => {
  1280. if (!song) return next("No song found with that id.");
  1281. return next(null, song);
  1282. },
  1283. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  1284. (song, user, next) => {
  1285. if (!user) return next("User does not exist.");
  1286. return this.module
  1287. .runJob(
  1288. "RUN_ACTION2",
  1289. {
  1290. session,
  1291. namespace: "playlists",
  1292. action: "removeSongFromPlaylist",
  1293. args: [youtubeId, user.dislikedSongsPlaylist]
  1294. },
  1295. this
  1296. )
  1297. .then(res => {
  1298. if (res.status === "error")
  1299. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  1300. return next(null, song, user.likedSongsPlaylist);
  1301. })
  1302. .catch(err => next(err));
  1303. },
  1304. (song, likedSongsPlaylist, next) => {
  1305. this.module
  1306. .runJob(
  1307. "RUN_ACTION2",
  1308. {
  1309. session,
  1310. namespace: "playlists",
  1311. action: "removeSongFromPlaylist",
  1312. args: [youtubeId, likedSongsPlaylist]
  1313. },
  1314. this
  1315. )
  1316. .then(res => {
  1317. if (res.status === "error")
  1318. return next("Unable to remove song from the 'Liked Songs' playlist.");
  1319. return next(null, song);
  1320. })
  1321. .catch(err => next(err));
  1322. },
  1323. (song, next) => {
  1324. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1325. .then(ratings => next(null, song, ratings))
  1326. .catch(err => next(err));
  1327. }
  1328. ],
  1329. async (err, song, { likes, dislikes }) => {
  1330. if (err) {
  1331. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1332. this.log(
  1333. "ERROR",
  1334. "SONGS_UNLIKE",
  1335. `User "${session.userId}" failed to unlike song ${youtubeId}. "${err}"`
  1336. );
  1337. return cb({ status: "error", message: err });
  1338. }
  1339. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1340. CacheModule.runJob("PUB", {
  1341. channel: "song.unlike",
  1342. value: JSON.stringify({
  1343. youtubeId,
  1344. userId: session.userId,
  1345. likes,
  1346. dislikes
  1347. })
  1348. });
  1349. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1350. userId: session.userId,
  1351. type: "song__unlike",
  1352. payload: {
  1353. message: `Removed <youtubeId>${song.title} by ${song.artists.join(
  1354. ", "
  1355. )}</youtubeId> from your Liked Songs`,
  1356. youtubeId,
  1357. thumbnail: song.thumbnail
  1358. }
  1359. });
  1360. return cb({
  1361. status: "success",
  1362. message: "You have successfully unliked this song."
  1363. });
  1364. }
  1365. );
  1366. }),
  1367. /**
  1368. * Gets user's own song ratings
  1369. *
  1370. * @param session
  1371. * @param youtubeId - the youtube id
  1372. * @param cb
  1373. */
  1374. getOwnSongRatings: isLoginRequired(async function getOwnSongRatings(session, youtubeId, cb) {
  1375. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  1376. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1377. async.waterfall(
  1378. [
  1379. next => songModel.findOne({ youtubeId }, next),
  1380. (song, next) => {
  1381. if (!song) return next("No song found with that id.");
  1382. return next(null);
  1383. },
  1384. next =>
  1385. playlistModel.findOne(
  1386. { createdBy: session.userId, displayName: "Liked Songs" },
  1387. (err, playlist) => {
  1388. if (err) return next(err);
  1389. if (!playlist) return next("'Liked Songs' playlist does not exist.");
  1390. let isLiked = false;
  1391. Object.values(playlist.songs).forEach(song => {
  1392. // song is found in 'liked songs' playlist
  1393. if (song.youtubeId === youtubeId) isLiked = true;
  1394. });
  1395. return next(null, isLiked);
  1396. }
  1397. ),
  1398. (isLiked, next) =>
  1399. playlistModel.findOne(
  1400. { createdBy: session.userId, displayName: "Disliked Songs" },
  1401. (err, playlist) => {
  1402. if (err) return next(err);
  1403. if (!playlist) return next("'Disliked Songs' playlist does not exist.");
  1404. const ratings = { isLiked, isDisliked: false };
  1405. Object.values(playlist.songs).forEach(song => {
  1406. // song is found in 'disliked songs' playlist
  1407. if (song.youtubeId === youtubeId) ratings.isDisliked = true;
  1408. });
  1409. return next(null, ratings);
  1410. }
  1411. )
  1412. ],
  1413. async (err, ratings) => {
  1414. if (err) {
  1415. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1416. this.log(
  1417. "ERROR",
  1418. "SONGS_GET_OWN_RATINGS",
  1419. `User "${session.userId}" failed to get ratings for ${youtubeId}. "${err}"`
  1420. );
  1421. return cb({ status: "error", message: err });
  1422. }
  1423. const { isLiked, isDisliked } = ratings;
  1424. return cb({
  1425. status: "success",
  1426. data: {
  1427. youtubeId,
  1428. liked: isLiked,
  1429. disliked: isDisliked
  1430. }
  1431. });
  1432. }
  1433. );
  1434. })
  1435. };