songs.js 40 KB

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