songs.js 44 KB

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