songs.js 41 KB

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