songs.js 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095
  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}`, "edit-songs"],
  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}`, "edit-songs"],
  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. DBModule.runJob(
  179. "GET_DATA",
  180. {
  181. page,
  182. pageSize,
  183. properties,
  184. sort,
  185. queries,
  186. operator,
  187. modelName: "song",
  188. blacklistedProperties: [],
  189. specialProperties: {
  190. requestedBy: [
  191. {
  192. $addFields: {
  193. requestedByOID: {
  194. $convert: {
  195. input: "$requestedBy",
  196. to: "objectId",
  197. onError: "unknown",
  198. onNull: "unknown"
  199. }
  200. }
  201. }
  202. },
  203. {
  204. $lookup: {
  205. from: "users",
  206. localField: "requestedByOID",
  207. foreignField: "_id",
  208. as: "requestedByUser"
  209. }
  210. },
  211. {
  212. $addFields: {
  213. requestedByUsername: {
  214. $ifNull: ["$requestedByUser.username", "unknown"]
  215. }
  216. }
  217. },
  218. {
  219. $project: {
  220. requestedByOID: 0,
  221. requestedByUser: 0
  222. }
  223. }
  224. ],
  225. verifiedBy: [
  226. {
  227. $addFields: {
  228. verifiedByOID: {
  229. $convert: {
  230. input: "$verifiedBy",
  231. to: "objectId",
  232. onError: "unknown",
  233. onNull: "unknown"
  234. }
  235. }
  236. }
  237. },
  238. {
  239. $lookup: {
  240. from: "users",
  241. localField: "verifiedByOID",
  242. foreignField: "_id",
  243. as: "verifiedByUser"
  244. }
  245. },
  246. {
  247. $unwind: {
  248. path: "$verifiedByUser",
  249. preserveNullAndEmptyArrays: true
  250. }
  251. },
  252. {
  253. $addFields: {
  254. verifiedByUsername: {
  255. $ifNull: ["$verifiedByUser.username", "unknown"]
  256. }
  257. }
  258. },
  259. {
  260. $project: {
  261. verifiedByOID: 0,
  262. verifiedByUser: 0
  263. }
  264. }
  265. ]
  266. },
  267. specialQueries: {
  268. requestedBy: newQuery => ({
  269. $or: [newQuery, { requestedByUsername: newQuery.requestedBy }]
  270. }),
  271. verifiedBy: newQuery => ({
  272. $or: [newQuery, { verifiedByUsername: newQuery.verifiedBy }]
  273. })
  274. }
  275. },
  276. this
  277. )
  278. .then(response => {
  279. next(null, response);
  280. })
  281. .catch(err => {
  282. next(err);
  283. });
  284. }
  285. ],
  286. async (err, response) => {
  287. if (err) {
  288. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  289. this.log("ERROR", "SONGS_GET_DATA", `Failed to get data from songs. "${err}"`);
  290. return cb({ status: "error", message: err });
  291. }
  292. this.log("SUCCESS", "SONGS_GET_DATA", `Got data from songs successfully.`);
  293. return cb({ status: "success", message: "Successfully got data from songs.", data: response });
  294. }
  295. );
  296. }),
  297. /**
  298. * Updates all songs
  299. *
  300. * @param {object} session - the session object automatically added by the websocket
  301. * @param cb
  302. */
  303. updateAll: isAdminRequired(async function updateAll(session, cb) {
  304. async.waterfall(
  305. [
  306. next => {
  307. SongsModule.runJob("UPDATE_ALL_SONGS", {}, this)
  308. .then(() => {
  309. next();
  310. })
  311. .catch(err => {
  312. next(err);
  313. });
  314. }
  315. ],
  316. async err => {
  317. if (err) {
  318. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  319. this.log("ERROR", "SONGS_UPDATE_ALL", `Failed to update all songs. "${err}"`);
  320. return cb({ status: "error", message: err });
  321. }
  322. this.log("SUCCESS", "SONGS_UPDATE_ALL", `Updated all songs successfully.`);
  323. return cb({ status: "success", message: "Successfully updated all songs." });
  324. }
  325. );
  326. }),
  327. /**
  328. * Recalculates all song ratings
  329. *
  330. * @param {object} session - the session object automatically added by the websocket
  331. * @param cb
  332. */
  333. recalculateAllRatings: isAdminRequired(async function recalculateAllRatings(session, cb) {
  334. async.waterfall(
  335. [
  336. next => {
  337. SongsModule.runJob("RECALCULATE_ALL_SONG_RATINGS", {}, this)
  338. .then(() => {
  339. next();
  340. })
  341. .catch(err => {
  342. next(err);
  343. });
  344. }
  345. ],
  346. async err => {
  347. if (err) {
  348. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  349. this.log(
  350. "ERROR",
  351. "SONGS_RECALCULATE_ALL_RATINGS",
  352. `Failed to recalculate all song ratings. "${err}"`
  353. );
  354. return cb({ status: "error", message: err });
  355. }
  356. this.log("SUCCESS", "SONGS_RECALCULATE_ALL_RATINGS", `Recalculated all song ratings successfully.`);
  357. return cb({ status: "success", message: "Successfully recalculated all song ratings." });
  358. }
  359. );
  360. }),
  361. /**
  362. * Gets a song from the Musare song id
  363. *
  364. * @param {object} session - the session object automatically added by the websocket
  365. * @param {string} songId - the song id
  366. * @param {Function} cb
  367. */
  368. getSongFromSongId: isAdminRequired(function getSongFromSongId(session, songId, cb) {
  369. async.waterfall(
  370. [
  371. next => {
  372. SongsModule.runJob("GET_SONG", { songId }, this)
  373. .then(response => next(null, response.song))
  374. .catch(err => next(err));
  375. }
  376. ],
  377. async (err, song) => {
  378. if (err) {
  379. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  380. this.log("ERROR", "SONGS_GET_SONG_FROM_MUSARE_ID", `Failed to get song ${songId}. "${err}"`);
  381. return cb({ status: "error", message: err });
  382. }
  383. this.log("SUCCESS", "SONGS_GET_SONG_FROM_MUSARE_ID", `Got song ${songId} successfully.`);
  384. return cb({ status: "success", data: { song } });
  385. }
  386. );
  387. }),
  388. /**
  389. * Gets multiple songs from the Musare song ids
  390. * At this time only used in EditSongs
  391. *
  392. * @param {object} session - the session object automatically added by the websocket
  393. * @param {Array} songIds - the song ids
  394. * @param {Function} cb
  395. */
  396. getSongsFromSongIds: isAdminRequired(function getSongFromSongId(session, songIds, cb) {
  397. async.waterfall(
  398. [
  399. next => {
  400. SongsModule.runJob(
  401. "GET_SONGS",
  402. {
  403. songIds,
  404. properties: ["youtubeId", "title", "artists", "thumbnail", "duration", "verified", "_id"]
  405. },
  406. this
  407. )
  408. .then(response => next(null, response.songs))
  409. .catch(err => next(err));
  410. }
  411. ],
  412. async (err, songs) => {
  413. if (err) {
  414. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  415. this.log("ERROR", "SONGS_GET_SONGS_FROM_MUSARE_IDS", `Failed to get songs. "${err}"`);
  416. return cb({ status: "error", message: err });
  417. }
  418. this.log("SUCCESS", "SONGS_GET_SONGS_FROM_MUSARE_IDS", `Got songs successfully.`);
  419. return cb({ status: "success", data: { songs } });
  420. }
  421. );
  422. }),
  423. /**
  424. * Creates a song
  425. *
  426. * @param {object} session - the session object automatically added by the websocket
  427. * @param {object} newSong - the song object
  428. * @param {Function} cb
  429. */
  430. create: isAdminRequired(async function create(session, newSong, cb) {
  431. async.waterfall(
  432. [
  433. next => {
  434. const song = new SongsModule.SongModel(newSong);
  435. song.save({ validateBeforeSave: true }, err => {
  436. if (err) return next(err, song);
  437. return next(null, song);
  438. });
  439. }
  440. ],
  441. async (err, song) => {
  442. if (err) {
  443. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  444. this.log("ERROR", "SONGS_CREATE", `Failed to create song "${JSON.stringify(song)}". "${err}"`);
  445. return cb({ status: "error", message: err });
  446. }
  447. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  448. this.log("SUCCESS", "SONGS_CREATE", `Successfully created song "${song._id}".`);
  449. return cb({
  450. status: "success",
  451. message: "Song has been successfully created",
  452. data: { song }
  453. });
  454. }
  455. );
  456. }),
  457. /**
  458. * Updates a song
  459. *
  460. * @param {object} session - the session object automatically added by the websocket
  461. * @param {string} songId - the song id
  462. * @param {object} song - the updated song object
  463. * @param {Function} cb
  464. */
  465. update: isAdminRequired(async function update(session, songId, song, cb) {
  466. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  467. let existingSong = null;
  468. async.waterfall(
  469. [
  470. next => {
  471. songModel.findOne({ _id: songId }, next);
  472. },
  473. (_existingSong, next) => {
  474. existingSong = _existingSong;
  475. songModel.updateOne({ _id: songId }, song, { runValidators: true }, next);
  476. },
  477. (res, next) => {
  478. SongsModule.runJob("UPDATE_SONG", { songId }, this)
  479. .then(song => {
  480. existingSong.genres
  481. .concat(song.genres)
  482. .filter((value, index, self) => self.indexOf(value) === index)
  483. .forEach(genre => {
  484. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
  485. .then(() => {})
  486. .catch(() => {});
  487. });
  488. next(null, song);
  489. })
  490. .catch(next);
  491. }
  492. ],
  493. async (err, song) => {
  494. if (err) {
  495. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  496. this.log("ERROR", "SONGS_UPDATE", `Failed to update song "${songId}". "${err}"`);
  497. return cb({ status: "error", message: err });
  498. }
  499. this.log("SUCCESS", "SONGS_UPDATE", `Successfully updated song "${songId}".`);
  500. return cb({
  501. status: "success",
  502. message: "Song has been successfully updated",
  503. data: { song }
  504. });
  505. }
  506. );
  507. }),
  508. /**
  509. * Removes a song
  510. *
  511. * @param session
  512. * @param songId - the song id
  513. * @param cb
  514. */
  515. remove: isAdminRequired(async function remove(session, songId, cb) {
  516. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  517. const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
  518. async.waterfall(
  519. [
  520. next => {
  521. songModel.findOne({ _id: songId }, next);
  522. },
  523. (song, next) => {
  524. PlaylistsModule.runJob("GET_PLAYLISTS_WITH_SONG", { songId }, this)
  525. .then(res => {
  526. async.eachLimit(
  527. res.playlists,
  528. 1,
  529. (playlist, next) => {
  530. WSModule.runJob(
  531. "RUN_ACTION2",
  532. {
  533. session,
  534. namespace: "playlists",
  535. action: "removeSongFromPlaylist",
  536. args: [song.youtubeId, playlist._id]
  537. },
  538. this
  539. )
  540. .then(res => {
  541. if (res.status === "error") next(res.message);
  542. else next();
  543. })
  544. .catch(err => {
  545. next(err);
  546. });
  547. },
  548. err => {
  549. if (err) next(err);
  550. else next(null, song);
  551. }
  552. );
  553. })
  554. .catch(err => next(err));
  555. },
  556. (song, next) => {
  557. stationModel.find({ "queue._id": songId }, (err, stations) => {
  558. if (err) next(err);
  559. else {
  560. async.eachLimit(
  561. stations,
  562. 1,
  563. (station, next) => {
  564. WSModule.runJob(
  565. "RUN_ACTION2",
  566. {
  567. session,
  568. namespace: "stations",
  569. action: "removeFromQueue",
  570. args: [station._id, song.youtubeId]
  571. },
  572. this
  573. )
  574. .then(res => {
  575. if (
  576. res.status === "error" &&
  577. res.message !== "Station not found" &&
  578. res.message !== "Song is not currently in the queue."
  579. )
  580. next(res.message);
  581. else next();
  582. })
  583. .catch(err => {
  584. next(err);
  585. });
  586. },
  587. err => {
  588. if (err) next(err);
  589. else next();
  590. }
  591. );
  592. }
  593. });
  594. },
  595. next => {
  596. stationModel.find({ "currentSong._id": songId }, (err, stations) => {
  597. if (err) next(err);
  598. else {
  599. async.eachLimit(
  600. stations,
  601. 1,
  602. (station, next) => {
  603. StationsModule.runJob(
  604. "SKIP_STATION",
  605. { stationId: station._id, natural: false },
  606. this
  607. )
  608. .then(() => {
  609. next();
  610. })
  611. .catch(err => {
  612. if (err.message === "Station not found.") next();
  613. else next(err);
  614. });
  615. },
  616. err => {
  617. if (err) next(err);
  618. else next();
  619. }
  620. );
  621. }
  622. });
  623. },
  624. next => {
  625. songModel.deleteOne({ _id: songId }, err => {
  626. if (err) next(err);
  627. else next();
  628. });
  629. },
  630. next => {
  631. CacheModule.runJob("HDEL", { table: "songs", key: songId }, this)
  632. .then(() => {
  633. next();
  634. })
  635. .catch(next);
  636. }
  637. ],
  638. async err => {
  639. if (err) {
  640. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  641. this.log("ERROR", "SONGS_REMOVE", `Failed to remove song "${songId}". "${err}"`);
  642. return cb({ status: "error", message: err });
  643. }
  644. this.log("SUCCESS", "SONGS_REMOVE", `Successfully removed song "${songId}".`);
  645. CacheModule.runJob("PUB", {
  646. channel: "song.removed",
  647. value: { songId }
  648. });
  649. return cb({
  650. status: "success",
  651. message: "Song has been successfully removed"
  652. });
  653. }
  654. );
  655. }),
  656. /**
  657. * Removes many songs
  658. *
  659. * @param session
  660. * @param songIds - array of song ids
  661. * @param cb
  662. */
  663. removeMany: isAdminRequired(async function remove(session, songIds, cb) {
  664. const successful = [];
  665. const failed = [];
  666. async.waterfall(
  667. [
  668. next => {
  669. async.eachLimit(
  670. songIds,
  671. 1,
  672. (songId, next) => {
  673. WSModule.runJob(
  674. "RUN_ACTION2",
  675. {
  676. session,
  677. namespace: "songs",
  678. action: "remove",
  679. args: [songId]
  680. },
  681. this
  682. )
  683. .then(res => {
  684. if (res.status === "error") failed.push(songId);
  685. else successful.push(songId);
  686. next();
  687. })
  688. .catch(err => {
  689. next(err);
  690. });
  691. },
  692. err => {
  693. if (err) next(err);
  694. else next();
  695. }
  696. );
  697. }
  698. ],
  699. async err => {
  700. if (err) {
  701. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  702. this.log("ERROR", "SONGS_REMOVE_MANY", `Failed to remove songs "${failed.join(", ")}". "${err}"`);
  703. return cb({ status: "error", message: err });
  704. }
  705. let message = "";
  706. if (successful.length === 1) message += `1 song has been successfully removed`;
  707. else message += `${successful.length} songs have been successfully removed`;
  708. if (failed.length > 0) {
  709. this.log("ERROR", "SONGS_REMOVE_MANY", `Failed to remove songs "${failed.join(", ")}". "${err}"`);
  710. if (failed.length === 1) message += `, failed to remove 1 song`;
  711. else message += `, failed to remove ${failed.length} songs`;
  712. }
  713. this.log("SUCCESS", "SONGS_REMOVE_MANY", `${message} "${successful.join(", ")}"`);
  714. return cb({
  715. status: "success",
  716. message
  717. });
  718. }
  719. );
  720. }),
  721. /**
  722. * Searches through official songs
  723. *
  724. * @param {object} session - the session object automatically added by the websocket
  725. * @param {string} query - the query
  726. * @param {string} page - the page
  727. * @param {Function} cb - gets called with the result
  728. */
  729. searchOfficial: isLoginRequired(async function searchOfficial(session, query, page, cb) {
  730. async.waterfall(
  731. [
  732. next => {
  733. if ((!query && query !== "") || typeof query !== "string") next("Invalid query.");
  734. else next();
  735. },
  736. next => {
  737. SongsModule.runJob("SEARCH", {
  738. query,
  739. includeVerified: true,
  740. trimmed: true,
  741. page
  742. })
  743. .then(response => {
  744. next(null, response);
  745. })
  746. .catch(err => {
  747. next(err);
  748. });
  749. }
  750. ],
  751. async (err, data) => {
  752. if (err) {
  753. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  754. this.log("ERROR", "SONGS_SEARCH_OFFICIAL", `Searching songs failed. "${err}"`);
  755. return cb({ status: "error", message: err });
  756. }
  757. this.log("SUCCESS", "SONGS_SEARCH_OFFICIAL", "Searching songs successful.");
  758. return cb({ status: "success", data });
  759. }
  760. );
  761. }),
  762. /**
  763. * Requests a song
  764. *
  765. * @param {object} session - the session object automatically added by the websocket
  766. * @param {string} youtubeId - the youtube id of the song that gets requested
  767. * @param {string} returnSong - returns the simple song
  768. * @param {Function} cb - gets called with the result
  769. */
  770. request: isLoginRequired(async function add(session, youtubeId, returnSong, cb) {
  771. SongsModule.runJob("REQUEST_SONG", { youtubeId, userId: session.userId }, this)
  772. .then(response => {
  773. this.log(
  774. "SUCCESS",
  775. "SONGS_REQUEST",
  776. `User "${session.userId}" successfully requested song "${youtubeId}".`
  777. );
  778. return cb({
  779. status: "success",
  780. message: "Successfully requested that song",
  781. song: returnSong ? response.song : null
  782. });
  783. })
  784. .catch(async _err => {
  785. const err = await UtilsModule.runJob("GET_ERROR", { error: _err }, this);
  786. this.log(
  787. "ERROR",
  788. "SONGS_REQUEST",
  789. `Requesting song "${youtubeId}" failed for user ${session.userId}. "${err}"`
  790. );
  791. return cb({ status: "error", message: err, song: returnSong && _err.data ? _err.data.song : null });
  792. });
  793. }),
  794. /**
  795. * Verifies a song
  796. *
  797. * @param session
  798. * @param songId - the song id
  799. * @param cb
  800. */
  801. verify: isAdminRequired(async function add(session, songId, cb) {
  802. const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  803. async.waterfall(
  804. [
  805. next => {
  806. SongModel.findOne({ _id: songId }, next);
  807. },
  808. (song, next) => {
  809. if (!song) return next("This song is not in the database.");
  810. return next(null, song);
  811. },
  812. (song, next) => {
  813. const oldStatus = false;
  814. song.verifiedBy = session.userId;
  815. song.verifiedAt = Date.now();
  816. song.verified = true;
  817. song.save(err => next(err, song, oldStatus));
  818. },
  819. (song, oldStatus, next) => {
  820. song.genres.forEach(genre => {
  821. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
  822. .then(() => {})
  823. .catch(() => {});
  824. });
  825. SongsModule.runJob("UPDATE_SONG", { songId: song._id, oldStatus });
  826. next(null, song, oldStatus);
  827. }
  828. ],
  829. async err => {
  830. if (err) {
  831. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  832. this.log("ERROR", "SONGS_VERIFY", `User "${session.userId}" failed to verify song. "${err}"`);
  833. return cb({ status: "error", message: err });
  834. }
  835. this.log("SUCCESS", "SONGS_VERIFY", `User "${session.userId}" successfully verified song "${songId}".`);
  836. return cb({
  837. status: "success",
  838. message: "Song has been verified successfully."
  839. });
  840. }
  841. );
  842. // TODO Check if video is in queue and Add the song to the appropriate stations
  843. }),
  844. /**
  845. * Verify many songs
  846. *
  847. * @param session
  848. * @param songIds - array of song ids
  849. * @param cb
  850. */
  851. verifyMany: isAdminRequired(async function verifyMany(session, songIds, cb) {
  852. const successful = [];
  853. const failed = [];
  854. async.waterfall(
  855. [
  856. next => {
  857. async.eachLimit(
  858. songIds,
  859. 1,
  860. (songId, next) => {
  861. WSModule.runJob(
  862. "RUN_ACTION2",
  863. {
  864. session,
  865. namespace: "songs",
  866. action: "verify",
  867. args: [songId]
  868. },
  869. this
  870. )
  871. .then(res => {
  872. if (res.status === "error") failed.push(songId);
  873. else successful.push(songId);
  874. next();
  875. })
  876. .catch(err => {
  877. next(err);
  878. });
  879. },
  880. err => {
  881. if (err) next(err);
  882. else next();
  883. }
  884. );
  885. }
  886. ],
  887. async err => {
  888. if (err) {
  889. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  890. this.log("ERROR", "SONGS_VERIFY_MANY", `Failed to verify songs "${failed.join(", ")}". "${err}"`);
  891. return cb({ status: "error", message: err });
  892. }
  893. let message = "";
  894. if (successful.length === 1) message += `1 song has been successfully verified`;
  895. else message += `${successful.length} songs have been successfully verified`;
  896. if (failed.length > 0) {
  897. this.log("ERROR", "SONGS_VERIFY_MANY", `Failed to verify songs "${failed.join(", ")}". "${err}"`);
  898. if (failed.length === 1) message += `, failed to verify 1 song`;
  899. else message += `, failed to verify ${failed.length} songs`;
  900. }
  901. this.log("SUCCESS", "SONGS_VERIFY_MANY", `${message} "${successful.join(", ")}"`);
  902. return cb({
  903. status: "success",
  904. message
  905. });
  906. }
  907. );
  908. }),
  909. /**
  910. * Un-verifies a song
  911. *
  912. * @param session
  913. * @param songId - the song id
  914. * @param cb
  915. */
  916. unverify: isAdminRequired(async function add(session, songId, cb) {
  917. const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  918. async.waterfall(
  919. [
  920. next => {
  921. SongModel.findOne({ _id: songId }, next);
  922. },
  923. (song, next) => {
  924. if (!song) return next("This song is not in the database.");
  925. return next(null, song);
  926. },
  927. (song, next) => {
  928. song.verified = false;
  929. song.save(err => {
  930. next(err, song);
  931. });
  932. },
  933. (song, next) => {
  934. song.genres.forEach(genre => {
  935. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
  936. .then(() => {})
  937. .catch(() => {});
  938. });
  939. SongsModule.runJob("UPDATE_SONG", { songId, oldStatus: true });
  940. next(null);
  941. }
  942. ],
  943. async err => {
  944. if (err) {
  945. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  946. this.log("ERROR", "SONGS_UNVERIFY", `User "${session.userId}" failed to verify song. "${err}"`);
  947. return cb({ status: "error", message: err });
  948. }
  949. this.log(
  950. "SUCCESS",
  951. "SONGS_UNVERIFY",
  952. `User "${session.userId}" successfully unverified song "${songId}".`
  953. );
  954. return cb({
  955. status: "success",
  956. message: "Song has been unverified successfully."
  957. });
  958. }
  959. );
  960. // TODO Check if video is in queue and Add the song to the appropriate stations
  961. }),
  962. /**
  963. * Unverify many songs
  964. *
  965. * @param session
  966. * @param songIds - array of song ids
  967. * @param cb
  968. */
  969. unverifyMany: isAdminRequired(async function unverifyMany(session, songIds, cb) {
  970. const successful = [];
  971. const failed = [];
  972. async.waterfall(
  973. [
  974. next => {
  975. async.eachLimit(
  976. songIds,
  977. 1,
  978. (songId, next) => {
  979. WSModule.runJob(
  980. "RUN_ACTION2",
  981. {
  982. session,
  983. namespace: "songs",
  984. action: "unverify",
  985. args: [songId]
  986. },
  987. this
  988. )
  989. .then(res => {
  990. if (res.status === "error") failed.push(songId);
  991. else successful.push(songId);
  992. next();
  993. })
  994. .catch(err => {
  995. next(err);
  996. });
  997. },
  998. err => {
  999. if (err) next(err);
  1000. else next();
  1001. }
  1002. );
  1003. }
  1004. ],
  1005. async err => {
  1006. if (err) {
  1007. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1008. this.log(
  1009. "ERROR",
  1010. "SONGS_UNVERIFY_MANY",
  1011. `Failed to unverify songs "${failed.join(", ")}". "${err}"`
  1012. );
  1013. return cb({ status: "error", message: err });
  1014. }
  1015. let message = "";
  1016. if (successful.length === 1) message += `1 song has been successfully unverified`;
  1017. else message += `${successful.length} songs have been successfully unverified`;
  1018. if (failed.length > 0) {
  1019. this.log(
  1020. "ERROR",
  1021. "SONGS_UNVERIFY_MANY",
  1022. `Failed to unverify songs "${failed.join(", ")}". "${err}"`
  1023. );
  1024. if (failed.length === 1) message += `, failed to unverify 1 song`;
  1025. else message += `, failed to unverify ${failed.length} songs`;
  1026. }
  1027. this.log("SUCCESS", "SONGS_UNVERIFY_MANY", `${message} "${successful.join(", ")}"`);
  1028. return cb({
  1029. status: "success",
  1030. message
  1031. });
  1032. }
  1033. );
  1034. }),
  1035. /**
  1036. * Requests a set of songs
  1037. *
  1038. * @param {object} session - the session object automatically added by the websocket
  1039. * @param {string} url - the url of the the YouTube playlist
  1040. * @param {boolean} musicOnly - whether to only get music from the playlist
  1041. * @param {Function} cb - gets called with the result
  1042. */
  1043. requestSet: isLoginRequired(function requestSet(session, url, musicOnly, returnSongs, cb) {
  1044. async.waterfall(
  1045. [
  1046. next => {
  1047. YouTubeModule.runJob(
  1048. "GET_PLAYLIST",
  1049. {
  1050. url,
  1051. musicOnly
  1052. },
  1053. this
  1054. )
  1055. .then(res => {
  1056. next(null, res.songs);
  1057. })
  1058. .catch(next);
  1059. },
  1060. (youtubeIds, next) => {
  1061. let successful = 0;
  1062. let songs = {};
  1063. let failed = 0;
  1064. let alreadyInDatabase = 0;
  1065. if (youtubeIds.length === 0) next();
  1066. async.eachOfLimit(
  1067. youtubeIds,
  1068. 1,
  1069. (youtubeId, index, next) => {
  1070. WSModule.runJob(
  1071. "RUN_ACTION2",
  1072. {
  1073. session,
  1074. namespace: "songs",
  1075. action: "request",
  1076. args: [youtubeId, returnSongs]
  1077. },
  1078. this
  1079. )
  1080. .then(res => {
  1081. if (res.status === "success") successful += 1;
  1082. else failed += 1;
  1083. if (res.message === "This song is already in the database.") alreadyInDatabase += 1;
  1084. if (res.song) songs[index] = res.song;
  1085. else songs[index] = null;
  1086. })
  1087. .catch(() => {
  1088. failed += 1;
  1089. })
  1090. .finally(() => {
  1091. next();
  1092. });
  1093. },
  1094. () => {
  1095. if (returnSongs)
  1096. songs = Object.keys(songs)
  1097. .sort()
  1098. .map(key => songs[key]);
  1099. next(null, { successful, failed, alreadyInDatabase, songs });
  1100. }
  1101. );
  1102. }
  1103. ],
  1104. async (err, response) => {
  1105. if (err) {
  1106. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1107. this.log(
  1108. "ERROR",
  1109. "REQUEST_SET",
  1110. `Importing a YouTube playlist to be requested failed for user "${session.userId}". "${err}"`
  1111. );
  1112. return cb({ status: "error", message: err });
  1113. }
  1114. this.log(
  1115. "SUCCESS",
  1116. "REQUEST_SET",
  1117. `Successfully imported a YouTube playlist to be requested for user "${session.userId}".`
  1118. );
  1119. return cb({
  1120. status: "success",
  1121. message: `Playlist is done importing. ${response.successful} were added succesfully, ${response.failed} failed (${response.alreadyInDatabase} were already in database)`,
  1122. songs: returnSongs ? response.songs : null
  1123. });
  1124. }
  1125. );
  1126. }),
  1127. /**
  1128. * Likes a song
  1129. *
  1130. * @param session
  1131. * @param youtubeId - the youtube id
  1132. * @param cb
  1133. */
  1134. like: isLoginRequired(async function like(session, youtubeId, cb) {
  1135. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1136. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1137. async.waterfall(
  1138. [
  1139. next => songModel.findOne({ youtubeId }, next),
  1140. (song, next) => {
  1141. if (!song) return next("No song found with that id.");
  1142. return next(null, song);
  1143. },
  1144. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  1145. (song, user, next) => {
  1146. if (!user) return next("User does not exist.");
  1147. return this.module
  1148. .runJob(
  1149. "RUN_ACTION2",
  1150. {
  1151. session,
  1152. namespace: "playlists",
  1153. action: "removeSongFromPlaylist",
  1154. args: [youtubeId, user.dislikedSongsPlaylist]
  1155. },
  1156. this
  1157. )
  1158. .then(res => {
  1159. if (res.status === "error")
  1160. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  1161. return next(null, song, user.likedSongsPlaylist);
  1162. })
  1163. .catch(err => next(err));
  1164. },
  1165. (song, likedSongsPlaylist, next) =>
  1166. this.module
  1167. .runJob(
  1168. "RUN_ACTION2",
  1169. {
  1170. session,
  1171. namespace: "playlists",
  1172. action: "addSongToPlaylist",
  1173. args: [false, youtubeId, likedSongsPlaylist]
  1174. },
  1175. this
  1176. )
  1177. .then(res => {
  1178. if (res.status === "error") {
  1179. if (res.message === "That song is already in the playlist")
  1180. return next("You have already liked this song.");
  1181. return next("Unable to add song to the 'Liked Songs' playlist.");
  1182. }
  1183. return next(null, song);
  1184. })
  1185. .catch(err => next(err)),
  1186. (song, next) => {
  1187. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1188. .then(ratings => next(null, song, ratings))
  1189. .catch(err => next(err));
  1190. }
  1191. ],
  1192. async (err, song, ratings) => {
  1193. if (err) {
  1194. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1195. this.log(
  1196. "ERROR",
  1197. "SONGS_LIKE",
  1198. `User "${session.userId}" failed to like song ${youtubeId}. "${err}"`
  1199. );
  1200. return cb({ status: "error", message: err });
  1201. }
  1202. const { likes, dislikes } = ratings;
  1203. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1204. CacheModule.runJob("PUB", {
  1205. channel: "song.like",
  1206. value: JSON.stringify({
  1207. youtubeId,
  1208. userId: session.userId,
  1209. likes,
  1210. dislikes
  1211. })
  1212. });
  1213. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1214. userId: session.userId,
  1215. type: "song__like",
  1216. payload: {
  1217. message: `Liked song <youtubeId>${song.title} by ${song.artists.join(", ")}</youtubeId>`,
  1218. youtubeId,
  1219. thumbnail: song.thumbnail
  1220. }
  1221. });
  1222. return cb({
  1223. status: "success",
  1224. message: "You have successfully liked this song."
  1225. });
  1226. }
  1227. );
  1228. }),
  1229. /**
  1230. * Dislikes a song
  1231. *
  1232. * @param session
  1233. * @param youtubeId - the youtube id
  1234. * @param cb
  1235. */
  1236. dislike: isLoginRequired(async function dislike(session, youtubeId, cb) {
  1237. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1238. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1239. async.waterfall(
  1240. [
  1241. next => {
  1242. songModel.findOne({ youtubeId }, next);
  1243. },
  1244. (song, next) => {
  1245. if (!song) return next("No song found with that id.");
  1246. return next(null, song);
  1247. },
  1248. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  1249. (song, user, next) => {
  1250. if (!user) return next("User does not exist.");
  1251. return this.module
  1252. .runJob(
  1253. "RUN_ACTION2",
  1254. {
  1255. session,
  1256. namespace: "playlists",
  1257. action: "removeSongFromPlaylist",
  1258. args: [youtubeId, user.likedSongsPlaylist]
  1259. },
  1260. this
  1261. )
  1262. .then(res => {
  1263. if (res.status === "error")
  1264. return next("Unable to remove song from the 'Liked Songs' playlist.");
  1265. return next(null, song, user.dislikedSongsPlaylist);
  1266. })
  1267. .catch(err => next(err));
  1268. },
  1269. (song, dislikedSongsPlaylist, next) =>
  1270. this.module
  1271. .runJob(
  1272. "RUN_ACTION2",
  1273. {
  1274. session,
  1275. namespace: "playlists",
  1276. action: "addSongToPlaylist",
  1277. args: [false, youtubeId, dislikedSongsPlaylist]
  1278. },
  1279. this
  1280. )
  1281. .then(res => {
  1282. if (res.status === "error") {
  1283. if (res.message === "That song is already in the playlist")
  1284. return next("You have already disliked this song.");
  1285. return next("Unable to add song to the 'Disliked Songs' playlist.");
  1286. }
  1287. return next(null, song);
  1288. })
  1289. .catch(err => next(err)),
  1290. (song, next) => {
  1291. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1292. .then(ratings => next(null, song, ratings))
  1293. .catch(err => next(err));
  1294. }
  1295. ],
  1296. async (err, song, ratings) => {
  1297. if (err) {
  1298. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1299. this.log(
  1300. "ERROR",
  1301. "SONGS_DISLIKE",
  1302. `User "${session.userId}" failed to dislike song ${youtubeId}. "${err}"`
  1303. );
  1304. return cb({ status: "error", message: err });
  1305. }
  1306. const { likes, dislikes } = ratings;
  1307. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1308. CacheModule.runJob("PUB", {
  1309. channel: "song.dislike",
  1310. value: JSON.stringify({
  1311. youtubeId,
  1312. userId: session.userId,
  1313. likes,
  1314. dislikes
  1315. })
  1316. });
  1317. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1318. userId: session.userId,
  1319. type: "song__dislike",
  1320. payload: {
  1321. message: `Disliked song <youtubeId>${song.title} by ${song.artists.join(", ")}</youtubeId>`,
  1322. youtubeId,
  1323. thumbnail: song.thumbnail
  1324. }
  1325. });
  1326. return cb({
  1327. status: "success",
  1328. message: "You have successfully disliked this song."
  1329. });
  1330. }
  1331. );
  1332. }),
  1333. /**
  1334. * Undislikes a song
  1335. *
  1336. * @param session
  1337. * @param youtubeId - the youtube id
  1338. * @param cb
  1339. */
  1340. undislike: isLoginRequired(async function undislike(session, youtubeId, cb) {
  1341. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1342. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1343. async.waterfall(
  1344. [
  1345. next => {
  1346. songModel.findOne({ youtubeId }, next);
  1347. },
  1348. (song, next) => {
  1349. if (!song) return next("No song found with that id.");
  1350. return next(null, song);
  1351. },
  1352. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  1353. (song, user, next) => {
  1354. if (!user) return next("User does not exist.");
  1355. return this.module
  1356. .runJob(
  1357. "RUN_ACTION2",
  1358. {
  1359. session,
  1360. namespace: "playlists",
  1361. action: "removeSongFromPlaylist",
  1362. args: [youtubeId, user.dislikedSongsPlaylist]
  1363. },
  1364. this
  1365. )
  1366. .then(res => {
  1367. if (res.status === "error")
  1368. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  1369. return next(null, song, user.likedSongsPlaylist);
  1370. })
  1371. .catch(err => next(err));
  1372. },
  1373. (song, likedSongsPlaylist, next) => {
  1374. this.module
  1375. .runJob(
  1376. "RUN_ACTION2",
  1377. {
  1378. session,
  1379. namespace: "playlists",
  1380. action: "removeSongFromPlaylist",
  1381. args: [youtubeId, likedSongsPlaylist]
  1382. },
  1383. this
  1384. )
  1385. .then(res => {
  1386. if (res.status === "error")
  1387. return next("Unable to remove song from the 'Liked Songs' playlist.");
  1388. return next(null, song);
  1389. })
  1390. .catch(err => next(err));
  1391. },
  1392. (song, next) => {
  1393. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1394. .then(ratings => next(null, song, ratings))
  1395. .catch(err => next(err));
  1396. }
  1397. ],
  1398. async (err, song, ratings) => {
  1399. if (err) {
  1400. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1401. this.log(
  1402. "ERROR",
  1403. "SONGS_UNDISLIKE",
  1404. `User "${session.userId}" failed to undislike song ${youtubeId}. "${err}"`
  1405. );
  1406. return cb({ status: "error", message: err });
  1407. }
  1408. const { likes, dislikes } = ratings;
  1409. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1410. CacheModule.runJob("PUB", {
  1411. channel: "song.undislike",
  1412. value: JSON.stringify({
  1413. youtubeId,
  1414. userId: session.userId,
  1415. likes,
  1416. dislikes
  1417. })
  1418. });
  1419. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1420. userId: session.userId,
  1421. type: "song__undislike",
  1422. payload: {
  1423. message: `Removed <youtubeId>${song.title} by ${song.artists.join(
  1424. ", "
  1425. )}</youtubeId> from your Disliked Songs`,
  1426. youtubeId,
  1427. thumbnail: song.thumbnail
  1428. }
  1429. });
  1430. return cb({
  1431. status: "success",
  1432. message: "You have successfully undisliked this song."
  1433. });
  1434. }
  1435. );
  1436. }),
  1437. /**
  1438. * Unlikes a song
  1439. *
  1440. * @param session
  1441. * @param youtubeId - the youtube id
  1442. * @param cb
  1443. */
  1444. unlike: isLoginRequired(async function unlike(session, youtubeId, cb) {
  1445. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1446. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1447. async.waterfall(
  1448. [
  1449. next => {
  1450. songModel.findOne({ youtubeId }, next);
  1451. },
  1452. (song, next) => {
  1453. if (!song) return next("No song found with that id.");
  1454. return next(null, song);
  1455. },
  1456. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  1457. (song, user, next) => {
  1458. if (!user) return next("User does not exist.");
  1459. return this.module
  1460. .runJob(
  1461. "RUN_ACTION2",
  1462. {
  1463. session,
  1464. namespace: "playlists",
  1465. action: "removeSongFromPlaylist",
  1466. args: [youtubeId, user.dislikedSongsPlaylist]
  1467. },
  1468. this
  1469. )
  1470. .then(res => {
  1471. if (res.status === "error")
  1472. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  1473. return next(null, song, user.likedSongsPlaylist);
  1474. })
  1475. .catch(err => next(err));
  1476. },
  1477. (song, likedSongsPlaylist, next) => {
  1478. this.module
  1479. .runJob(
  1480. "RUN_ACTION2",
  1481. {
  1482. session,
  1483. namespace: "playlists",
  1484. action: "removeSongFromPlaylist",
  1485. args: [youtubeId, likedSongsPlaylist]
  1486. },
  1487. this
  1488. )
  1489. .then(res => {
  1490. if (res.status === "error")
  1491. return next("Unable to remove song from the 'Liked Songs' playlist.");
  1492. return next(null, song);
  1493. })
  1494. .catch(err => next(err));
  1495. },
  1496. (song, next) => {
  1497. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1498. .then(ratings => next(null, song, ratings))
  1499. .catch(err => next(err));
  1500. }
  1501. ],
  1502. async (err, song, ratings) => {
  1503. if (err) {
  1504. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1505. this.log(
  1506. "ERROR",
  1507. "SONGS_UNLIKE",
  1508. `User "${session.userId}" failed to unlike song ${youtubeId}. "${err}"`
  1509. );
  1510. return cb({ status: "error", message: err });
  1511. }
  1512. const { likes, dislikes } = ratings;
  1513. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1514. CacheModule.runJob("PUB", {
  1515. channel: "song.unlike",
  1516. value: JSON.stringify({
  1517. youtubeId,
  1518. userId: session.userId,
  1519. likes,
  1520. dislikes
  1521. })
  1522. });
  1523. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1524. userId: session.userId,
  1525. type: "song__unlike",
  1526. payload: {
  1527. message: `Removed <youtubeId>${song.title} by ${song.artists.join(
  1528. ", "
  1529. )}</youtubeId> from your Liked Songs`,
  1530. youtubeId,
  1531. thumbnail: song.thumbnail
  1532. }
  1533. });
  1534. return cb({
  1535. status: "success",
  1536. message: "You have successfully unliked this song."
  1537. });
  1538. }
  1539. );
  1540. }),
  1541. /**
  1542. * Gets song ratings
  1543. *
  1544. * @param session
  1545. * @param songId - the Musare song id
  1546. * @param cb
  1547. */
  1548. getSongRatings: isLoginRequired(async function getSongRatings(session, songId, cb) {
  1549. async.waterfall(
  1550. [
  1551. next => {
  1552. SongsModule.runJob("GET_SONG", { songId }, this)
  1553. .then(res => next(null, res.song))
  1554. .catch(next);
  1555. },
  1556. (song, next) => {
  1557. next(null, {
  1558. likes: song.likes,
  1559. dislikes: song.dislikes
  1560. });
  1561. }
  1562. ],
  1563. async (err, ratings) => {
  1564. if (err) {
  1565. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1566. this.log(
  1567. "ERROR",
  1568. "SONGS_GET_RATINGS",
  1569. `User "${session.userId}" failed to get ratings for ${songId}. "${err}"`
  1570. );
  1571. return cb({ status: "error", message: err });
  1572. }
  1573. const { likes, dislikes } = ratings;
  1574. return cb({
  1575. status: "success",
  1576. data: {
  1577. likes,
  1578. dislikes
  1579. }
  1580. });
  1581. }
  1582. );
  1583. }),
  1584. /**
  1585. * Gets user's own song ratings
  1586. *
  1587. * @param session
  1588. * @param youtubeId - the youtube id
  1589. * @param cb
  1590. */
  1591. getOwnSongRatings: isLoginRequired(async function getOwnSongRatings(session, youtubeId, cb) {
  1592. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  1593. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1594. async.waterfall(
  1595. [
  1596. next => songModel.findOne({ youtubeId }, next),
  1597. (song, next) => {
  1598. if (!song) return next("No song found with that id.");
  1599. return next(null);
  1600. },
  1601. next =>
  1602. playlistModel.findOne(
  1603. { createdBy: session.userId, displayName: "Liked Songs" },
  1604. (err, playlist) => {
  1605. if (err) return next(err);
  1606. if (!playlist) return next("'Liked Songs' playlist does not exist.");
  1607. let isLiked = false;
  1608. Object.values(playlist.songs).forEach(song => {
  1609. // song is found in 'liked songs' playlist
  1610. if (song.youtubeId === youtubeId) isLiked = true;
  1611. });
  1612. return next(null, isLiked);
  1613. }
  1614. ),
  1615. (isLiked, next) =>
  1616. playlistModel.findOne(
  1617. { createdBy: session.userId, displayName: "Disliked Songs" },
  1618. (err, playlist) => {
  1619. if (err) return next(err);
  1620. if (!playlist) return next("'Disliked Songs' playlist does not exist.");
  1621. const ratings = { isLiked, isDisliked: false };
  1622. Object.values(playlist.songs).forEach(song => {
  1623. // song is found in 'disliked songs' playlist
  1624. if (song.youtubeId === youtubeId) ratings.isDisliked = true;
  1625. });
  1626. return next(null, ratings);
  1627. }
  1628. )
  1629. ],
  1630. async (err, ratings) => {
  1631. if (err) {
  1632. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1633. this.log(
  1634. "ERROR",
  1635. "SONGS_GET_OWN_RATINGS",
  1636. `User "${session.userId}" failed to get ratings for ${youtubeId}. "${err}"`
  1637. );
  1638. return cb({ status: "error", message: err });
  1639. }
  1640. const { isLiked, isDisliked } = ratings;
  1641. return cb({
  1642. status: "success",
  1643. data: {
  1644. youtubeId,
  1645. liked: isLiked,
  1646. disliked: isDisliked
  1647. }
  1648. });
  1649. }
  1650. );
  1651. }),
  1652. /**
  1653. * Gets a list of all genres
  1654. *
  1655. * @param session
  1656. * @param cb
  1657. */
  1658. getGenres: isAdminRequired(function getGenres(session, cb) {
  1659. async.waterfall(
  1660. [
  1661. next => {
  1662. SongsModule.runJob("GET_GENRES", this)
  1663. .then(res => {
  1664. next(null, res.genres);
  1665. })
  1666. .catch(next);
  1667. }
  1668. ],
  1669. async (err, genres) => {
  1670. if (err && err !== true) {
  1671. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1672. this.log("ERROR", "GET_GENRES", `User ${session.userId} failed to get genres. '${err}'`);
  1673. cb({ status: "error", message: err });
  1674. } else {
  1675. this.log("SUCCESS", "GET_GENRES", `User ${session.userId} has successfully got the genres.`);
  1676. cb({
  1677. status: "success",
  1678. message: "Successfully got genres.",
  1679. data: {
  1680. items: genres
  1681. }
  1682. });
  1683. }
  1684. }
  1685. );
  1686. }),
  1687. /**
  1688. * Bulk update genres for selected songs
  1689. *
  1690. * @param session
  1691. * @param method Whether to add, remove or replace genres
  1692. * @param genres Array of genres to apply
  1693. * @param songIds Array of songIds to apply genres to
  1694. * @param cb
  1695. */
  1696. editGenres: isAdminRequired(async function editGenres(session, method, genres, songIds, cb) {
  1697. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1698. async.waterfall(
  1699. [
  1700. next => {
  1701. songModel.find({ _id: { $in: songIds } }, next);
  1702. },
  1703. (songs, next) => {
  1704. const songsFound = songs.map(song => song._id);
  1705. if (songsFound.length > 0) next(null, songsFound);
  1706. else next("None of the specified songs were found.");
  1707. },
  1708. (songsFound, next) => {
  1709. const query = {};
  1710. if (method === "add") {
  1711. query.$push = { genres: { $each: genres } };
  1712. } else if (method === "remove") {
  1713. query.$pullAll = { genres };
  1714. } else if (method === "replace") {
  1715. query.$set = { genres };
  1716. } else {
  1717. next("Invalid method.");
  1718. }
  1719. songModel.updateMany({ _id: { $in: songsFound } }, query, { runValidators: true }, err => {
  1720. if (err) next(err);
  1721. SongsModule.runJob("UPDATE_SONGS", { songIds: songsFound });
  1722. });
  1723. }
  1724. ],
  1725. async err => {
  1726. if (err && err !== true) {
  1727. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1728. this.log("ERROR", "EDIT_GENRES", `User ${session.userId} failed to edit genres. '${err}'`);
  1729. cb({ status: "error", message: err });
  1730. } else {
  1731. this.log("SUCCESS", "EDIT_GENRES", `User ${session.userId} has successfully edited genres.`);
  1732. cb({
  1733. status: "success",
  1734. message: "Successfully edited genres."
  1735. });
  1736. }
  1737. }
  1738. );
  1739. }),
  1740. /**
  1741. * Gets a list of all artists
  1742. *
  1743. * @param session
  1744. * @param cb
  1745. */
  1746. getArtists: isAdminRequired(function getArtists(session, cb) {
  1747. async.waterfall(
  1748. [
  1749. next => {
  1750. SongsModule.runJob("GET_ARTISTS", this)
  1751. .then(res => {
  1752. next(null, res.artists);
  1753. })
  1754. .catch(next);
  1755. }
  1756. ],
  1757. async (err, artists) => {
  1758. if (err && err !== true) {
  1759. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1760. this.log("ERROR", "GET_ARTISTS", `User ${session.userId} failed to get artists. '${err}'`);
  1761. cb({ status: "error", message: err });
  1762. } else {
  1763. this.log("SUCCESS", "GET_ARTISTS", `User ${session.userId} has successfully got the artists.`);
  1764. cb({
  1765. status: "success",
  1766. message: "Successfully got artists.",
  1767. data: {
  1768. items: artists
  1769. }
  1770. });
  1771. }
  1772. }
  1773. );
  1774. }),
  1775. /**
  1776. * Bulk update artists for selected songs
  1777. *
  1778. * @param session
  1779. * @param method Whether to add, remove or replace artists
  1780. * @param artists Array of artists to apply
  1781. * @param songIds Array of songIds to apply artists to
  1782. * @param cb
  1783. */
  1784. editArtists: isAdminRequired(async function editArtists(session, method, artists, songIds, cb) {
  1785. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1786. async.waterfall(
  1787. [
  1788. next => {
  1789. songModel.find({ _id: { $in: songIds } }, next);
  1790. },
  1791. (songs, next) => {
  1792. const songsFound = songs.map(song => song._id);
  1793. if (songsFound.length > 0) next(null, songsFound);
  1794. else next("None of the specified songs were found.");
  1795. },
  1796. (songsFound, next) => {
  1797. const query = {};
  1798. if (method === "add") {
  1799. query.$push = { artists: { $each: artists } };
  1800. } else if (method === "remove") {
  1801. query.$pullAll = { artists };
  1802. } else if (method === "replace") {
  1803. query.$set = { artists };
  1804. } else {
  1805. next("Invalid method.");
  1806. }
  1807. songModel.updateMany({ _id: { $in: songsFound } }, query, { runValidators: true }, err => {
  1808. if (err) next(err);
  1809. SongsModule.runJob("UPDATE_SONGS", { songIds: songsFound });
  1810. });
  1811. }
  1812. ],
  1813. async err => {
  1814. if (err && err !== true) {
  1815. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1816. this.log("ERROR", "EDIT_ARTISTS", `User ${session.userId} failed to edit artists. '${err}'`);
  1817. cb({ status: "error", message: err });
  1818. } else {
  1819. this.log("SUCCESS", "EDIT_ARTISTS", `User ${session.userId} has successfully edited artists.`);
  1820. cb({
  1821. status: "success",
  1822. message: "Successfully edited artists."
  1823. });
  1824. }
  1825. }
  1826. );
  1827. }),
  1828. /**
  1829. * Gets a list of all tags
  1830. *
  1831. * @param session
  1832. * @param cb
  1833. */
  1834. getTags: isAdminRequired(function getTags(session, cb) {
  1835. async.waterfall(
  1836. [
  1837. next => {
  1838. SongsModule.runJob("GET_TAGS", this)
  1839. .then(res => {
  1840. next(null, res.tags);
  1841. })
  1842. .catch(next);
  1843. }
  1844. ],
  1845. async (err, tags) => {
  1846. if (err && err !== true) {
  1847. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1848. this.log("ERROR", "GET_TAGS", `User ${session.userId} failed to get tags. '${err}'`);
  1849. cb({ status: "error", message: err });
  1850. } else {
  1851. this.log("SUCCESS", "GET_TAGS", `User ${session.userId} has successfully got the tags.`);
  1852. cb({
  1853. status: "success",
  1854. message: "Successfully got tags.",
  1855. data: {
  1856. items: tags
  1857. }
  1858. });
  1859. }
  1860. }
  1861. );
  1862. }),
  1863. /**
  1864. * Bulk update tags for selected songs
  1865. *
  1866. * @param session
  1867. * @param method Whether to add, remove or replace tags
  1868. * @param tags Array of tags to apply
  1869. * @param songIds Array of songIds to apply tags to
  1870. * @param cb
  1871. */
  1872. editTags: isAdminRequired(async function editTags(session, method, tags, songIds, cb) {
  1873. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1874. async.waterfall(
  1875. [
  1876. next => {
  1877. songModel.find({ _id: { $in: songIds } }, next);
  1878. },
  1879. (songs, next) => {
  1880. const songsFound = songs.map(song => song._id);
  1881. if (songsFound.length > 0) next(null, songsFound);
  1882. else next("None of the specified songs were found.");
  1883. },
  1884. (songsFound, next) => {
  1885. const query = {};
  1886. if (method === "add") {
  1887. query.$push = { tags: { $each: tags } };
  1888. } else if (method === "remove") {
  1889. query.$pullAll = { tags };
  1890. } else if (method === "replace") {
  1891. query.$set = { tags };
  1892. } else {
  1893. next("Invalid method.");
  1894. }
  1895. songModel.updateMany({ _id: { $in: songsFound } }, query, { runValidators: true }, err => {
  1896. if (err) next(err);
  1897. SongsModule.runJob("UPDATE_SONGS", { songIds: songsFound });
  1898. });
  1899. }
  1900. ],
  1901. async err => {
  1902. if (err && err !== true) {
  1903. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1904. this.log("ERROR", "EDIT_TAGS", `User ${session.userId} failed to edit tags. '${err}'`);
  1905. cb({ status: "error", message: err });
  1906. } else {
  1907. this.log("SUCCESS", "EDIT_TAGS", `User ${session.userId} has successfully edited tags.`);
  1908. cb({
  1909. status: "success",
  1910. message: "Successfully edited tags."
  1911. });
  1912. }
  1913. }
  1914. );
  1915. })
  1916. };