songs.js 40 KB

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