songs.js 39 KB

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