songs.js 40 KB

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