songs.js 52 KB

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