songs.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858
  1. import async from "async";
  2. import mongoose from "mongoose";
  3. import CoreClass from "../core";
  4. let SongsModule;
  5. let CacheModule;
  6. let DBModule;
  7. let UtilsModule;
  8. let YouTubeModule;
  9. let StationsModule;
  10. let PlaylistsModule;
  11. class _SongsModule extends CoreClass {
  12. // eslint-disable-next-line require-jsdoc
  13. constructor() {
  14. super("songs");
  15. SongsModule = this;
  16. }
  17. /**
  18. * Initialises the songs module
  19. *
  20. * @returns {Promise} - returns promise (reject, resolve)
  21. */
  22. async initialize() {
  23. this.setStage(1);
  24. CacheModule = this.moduleManager.modules.cache;
  25. DBModule = this.moduleManager.modules.db;
  26. UtilsModule = this.moduleManager.modules.utils;
  27. YouTubeModule = this.moduleManager.modules.youtube;
  28. StationsModule = this.moduleManager.modules.stations;
  29. PlaylistsModule = this.moduleManager.modules.playlists;
  30. this.SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" });
  31. this.SongSchemaCache = await CacheModule.runJob("GET_SCHEMA", { schemaName: "song" });
  32. this.setStage(2);
  33. return new Promise((resolve, reject) =>
  34. async.waterfall(
  35. [
  36. next => {
  37. this.setStage(2);
  38. CacheModule.runJob("HGETALL", { table: "songs" })
  39. .then(songs => {
  40. next(null, songs);
  41. })
  42. .catch(next);
  43. },
  44. (songs, next) => {
  45. this.setStage(3);
  46. if (!songs) return next();
  47. const songIds = Object.keys(songs);
  48. return async.each(
  49. songIds,
  50. (songId, next) => {
  51. SongsModule.SongModel.findOne({ songId }, (err, song) => {
  52. if (err) next(err);
  53. else if (!song)
  54. CacheModule.runJob("HDEL", {
  55. table: "songs",
  56. key: songId
  57. })
  58. .then(() => next())
  59. .catch(next);
  60. else next();
  61. });
  62. },
  63. next
  64. );
  65. },
  66. next => {
  67. this.setStage(4);
  68. SongsModule.SongModel.find({}, next);
  69. },
  70. (songs, next) => {
  71. this.setStage(5);
  72. async.each(
  73. songs,
  74. (song, next) => {
  75. CacheModule.runJob("HSET", {
  76. table: "songs",
  77. key: song.songId,
  78. value: SongsModule.SongSchemaCache(song)
  79. })
  80. .then(() => next())
  81. .catch(next);
  82. },
  83. next
  84. );
  85. }
  86. ],
  87. async err => {
  88. if (err) {
  89. err = await UtilsModule.runJob("GET_ERROR", { error: err });
  90. reject(new Error(err));
  91. } else resolve();
  92. }
  93. )
  94. );
  95. }
  96. /**
  97. * Gets a song by id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
  98. *
  99. * @param {object} payload - object containing the payload
  100. * @param {string} payload.id - the id of the song we are trying to get
  101. * @returns {Promise} - returns a promise (resolve, reject)
  102. */
  103. GET_SONG(payload) {
  104. return new Promise((resolve, reject) =>
  105. async.waterfall(
  106. [
  107. next => {
  108. if (!mongoose.Types.ObjectId.isValid(payload.id)) return next("Id is not a valid ObjectId.");
  109. return CacheModule.runJob("HGET", { table: "songs", key: payload.id }, this)
  110. .then(song => next(null, song))
  111. .catch(next);
  112. },
  113. (song, next) => {
  114. if (song) return next(true, song);
  115. return SongsModule.SongModel.findOne({ _id: payload.id }, next);
  116. },
  117. (song, next) => {
  118. if (song) {
  119. CacheModule.runJob(
  120. "HSET",
  121. {
  122. table: "songs",
  123. key: payload.id,
  124. value: song
  125. },
  126. this
  127. ).then(song => next(null, song));
  128. } else next("Song not found.");
  129. }
  130. ],
  131. (err, song) => {
  132. if (err && err !== true) return reject(new Error(err));
  133. return resolve({ song });
  134. }
  135. )
  136. );
  137. }
  138. /**
  139. * Makes sure that if a song is not currently in the songs db, to add it
  140. *
  141. * @param {object} payload - an object containing the payload
  142. * @param {string} payload.songId - the youtube song id of the song we are trying to ensure is in the songs db
  143. * @returns {Promise} - returns a promise (resolve, reject)
  144. */
  145. ENSURE_SONG_EXISTS_BY_SONG_ID(payload) {
  146. return new Promise((resolve, reject) =>
  147. async.waterfall(
  148. [
  149. next => {
  150. SongsModule.SongModel.findOne({ songId: payload.songId }, next);
  151. },
  152. (song, next) => {
  153. if (song && song.duration > 0) next(true, song);
  154. else {
  155. YouTubeModule.runJob("GET_SONG", { songId: payload.songId, userId: payload.userId }, this)
  156. .then(response => {
  157. next(null, song, response.song);
  158. })
  159. .catch(next);
  160. }
  161. // else if (song && song.duration <= 0) {
  162. // YouTubeModule.runJob("GET_SONG", { songId: payload.songId }, this)
  163. // .then(response => next(null, { ...response.song }, false))
  164. // .catch(next);
  165. // } else {
  166. // YouTubeModule.runJob("GET_SONG", { songId: payload.songId }, this)
  167. // .then(response => next(null, { ...response.song }, false))
  168. // .catch(next);
  169. // }
  170. },
  171. (song, youtubeSong, next) => {
  172. if (song && song.duration <= 0) {
  173. song.duration = youtubeSong.duration;
  174. song.save({ validateBeforeSave: true }, err => {
  175. if (err) return next(err, song);
  176. return next(null, song);
  177. });
  178. } else {
  179. const song = new SongsModule.SongModel({ ...youtubeSong });
  180. song.save({ validateBeforeSave: true }, err => {
  181. if (err) return next(err, song);
  182. return next(null, song);
  183. });
  184. }
  185. }
  186. ],
  187. (err, song) => {
  188. if (err && err !== true) return reject(new Error(err));
  189. return resolve({ song });
  190. }
  191. )
  192. );
  193. }
  194. /**
  195. * Gets a song by song id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
  196. *
  197. * @param {object} payload - an object containing the payload
  198. * @param {string} payload.songId - the mongo id of the song we are trying to get
  199. * @returns {Promise} - returns a promise (resolve, reject)
  200. */
  201. GET_SONG_FROM_ID(payload) {
  202. return new Promise((resolve, reject) =>
  203. async.waterfall(
  204. [
  205. next => {
  206. SongsModule.SongModel.findOne({ songId: payload.songId }, next);
  207. }
  208. ],
  209. (err, song) => {
  210. if (err && err !== true) return reject(new Error(err));
  211. return resolve({ song });
  212. }
  213. )
  214. );
  215. }
  216. /**
  217. * Gets a song from id from Mongo and updates the cache with it
  218. *
  219. * @param {object} payload - an object containing the payload
  220. * @param {string} payload.songId - the id of the song we are trying to update
  221. * @returns {Promise} - returns a promise (resolve, reject)
  222. */
  223. UPDATE_SONG(payload) {
  224. return new Promise((resolve, reject) =>
  225. async.waterfall(
  226. [
  227. next => {
  228. SongsModule.SongModel.findOne({ _id: payload.songId }, next);
  229. },
  230. (song, next) => {
  231. if (!song) {
  232. CacheModule.runJob("HDEL", {
  233. table: "songs",
  234. key: payload.songId
  235. });
  236. return next("Song not found.");
  237. }
  238. return CacheModule.runJob(
  239. "HSET",
  240. {
  241. table: "songs",
  242. key: payload.songId,
  243. value: song
  244. },
  245. this
  246. )
  247. .then(song => {
  248. next(null, song);
  249. })
  250. .catch(next);
  251. },
  252. (song, next) => {
  253. next(null, song);
  254. const { _id, songId, title, artists, thumbnail, duration, status } = song;
  255. const trimmedSong = {
  256. _id,
  257. songId,
  258. title,
  259. artists,
  260. thumbnail,
  261. duration,
  262. status
  263. };
  264. this.log("INFO", `Going to update playlists and stations now for song ${_id}`);
  265. DBModule.runJob("GET_MODEL", { modelName: "playlist" }).then(playlistModel => {
  266. playlistModel.updateMany(
  267. { "songs._id": song._id },
  268. { $set: { "songs.$": trimmedSong } },
  269. err => {
  270. if (err) this.log("ERROR", err);
  271. else
  272. playlistModel.find({ "songs._id": song._id }, (err, playlists) => {
  273. playlists.forEach(playlist => {
  274. PlaylistsModule.runJob("UPDATE_PLAYLIST", {
  275. playlistId: playlist._id
  276. });
  277. });
  278. });
  279. }
  280. );
  281. });
  282. DBModule.runJob("GET_MODEL", { modelName: "station" }).then(stationModel => {
  283. stationModel.updateMany(
  284. { "queue._id": song._id },
  285. {
  286. $set: {
  287. "queue.$.songId": songId,
  288. "queue.$.title": title,
  289. "queue.$.artists": artists,
  290. "queue.$.thumbnail": thumbnail,
  291. "queue.$.duration": duration,
  292. "queue.$.status": status
  293. }
  294. },
  295. err => {
  296. if (err) this.log("ERROR", err);
  297. else
  298. stationModel.find({ "queue._id": song._id }, (err, stations) => {
  299. stations.forEach(station => {
  300. StationsModule.runJob("UPDATE_STATION", { stationId: station._id });
  301. });
  302. });
  303. }
  304. );
  305. });
  306. },
  307. (song, next) => {
  308. async.eachLimit(
  309. song.genres,
  310. 1,
  311. (genre, next) => {
  312. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre }, this)
  313. .then(() => {
  314. next();
  315. })
  316. .catch(err => next(err));
  317. },
  318. err => {
  319. next(err, song);
  320. }
  321. );
  322. }
  323. ],
  324. (err, song) => {
  325. if (err && err !== true) return reject(new Error(err));
  326. return resolve(song);
  327. }
  328. )
  329. );
  330. }
  331. /**
  332. * Deletes song from id from Mongo and cache
  333. *
  334. * @param {object} payload - returns an object containing the payload
  335. * @param {string} payload.songId - the id of the song we are trying to delete
  336. * @returns {Promise} - returns a promise (resolve, reject)
  337. */
  338. DELETE_SONG(payload) {
  339. return new Promise((resolve, reject) =>
  340. async.waterfall(
  341. [
  342. next => {
  343. SongsModule.SongModel.deleteOne({ songId: payload.songId }, next);
  344. },
  345. next => {
  346. CacheModule.runJob(
  347. "HDEL",
  348. {
  349. table: "songs",
  350. key: payload.songId
  351. },
  352. this
  353. )
  354. .then(() => next())
  355. .catch(next);
  356. }
  357. ],
  358. err => {
  359. if (err && err !== true) return reject(new Error(err));
  360. return resolve();
  361. }
  362. )
  363. );
  364. }
  365. /**
  366. * Recalculates dislikes and likes for a song
  367. *
  368. * @param {object} payload - returns an object containing the payload
  369. * @param {string} payload.musareSongId - the (musare) id of the song
  370. * @param {string} payload.songId - the (mongodb) id of the song
  371. * @returns {Promise} - returns a promise (resolve, reject)
  372. */
  373. async RECALCULATE_SONG_RATINGS(payload) {
  374. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  375. return new Promise((resolve, reject) => {
  376. async.waterfall(
  377. [
  378. next => {
  379. playlistModel.countDocuments(
  380. { songs: { $elemMatch: { songId: payload.musareSongId } }, displayName: "Liked Songs" },
  381. (err, likes) => {
  382. if (err) return next(err);
  383. return next(null, likes);
  384. }
  385. );
  386. },
  387. (likes, next) => {
  388. playlistModel.countDocuments(
  389. { songs: { $elemMatch: { songId: payload.musareSongId } }, displayName: "Disliked Songs" },
  390. (err, dislikes) => {
  391. if (err) return next(err);
  392. return next(err, { likes, dislikes });
  393. }
  394. );
  395. },
  396. ({ likes, dislikes }, next) => {
  397. SongsModule.SongModel.updateOne(
  398. { _id: payload.songId },
  399. {
  400. $set: {
  401. likes,
  402. dislikes
  403. }
  404. },
  405. err => next(err, { likes, dislikes })
  406. );
  407. }
  408. ],
  409. (err, { likes, dislikes }) => {
  410. if (err) return reject(new Error(err));
  411. return resolve({ likes, dislikes });
  412. }
  413. );
  414. });
  415. }
  416. /**
  417. * Gets an array of all genres
  418. *
  419. * @returns {Promise} - returns a promise (resolve, reject)
  420. */
  421. GET_ALL_GENRES() {
  422. return new Promise((resolve, reject) =>
  423. async.waterfall(
  424. [
  425. next => {
  426. SongsModule.SongModel.find({ status: "verified" }, { genres: 1, _id: false }, next);
  427. },
  428. (songs, next) => {
  429. let allGenres = [];
  430. songs.forEach(song => {
  431. allGenres = allGenres.concat(song.genres);
  432. });
  433. const lowerCaseGenres = allGenres.map(genre => genre.toLowerCase());
  434. const uniqueGenres = lowerCaseGenres.filter(
  435. (value, index, self) => self.indexOf(value) === index
  436. );
  437. next(null, uniqueGenres);
  438. }
  439. ],
  440. (err, genres) => {
  441. if (err && err !== true) return reject(new Error(err));
  442. return resolve({ genres });
  443. }
  444. )
  445. );
  446. }
  447. /**
  448. * Gets an array of all songs with a specific genre
  449. *
  450. * @param {object} payload - returns an object containing the payload
  451. * @param {string} payload.genre - the genre
  452. * @returns {Promise} - returns a promise (resolve, reject)
  453. */
  454. GET_ALL_SONGS_WITH_GENRE(payload) {
  455. return new Promise((resolve, reject) =>
  456. async.waterfall(
  457. [
  458. next => {
  459. SongsModule.SongModel.find(
  460. {
  461. status: "verified",
  462. genres: { $regex: new RegExp(`^${payload.genre.toLowerCase()}$`, "i") }
  463. },
  464. next
  465. );
  466. }
  467. ],
  468. (err, songs) => {
  469. if (err && err !== true) return reject(new Error(err));
  470. return resolve({ songs });
  471. }
  472. )
  473. );
  474. }
  475. // runjob songs GET_ORPHANED_PLAYLIST_SONGS {}
  476. /**
  477. * Gets a orphaned playlist songs
  478. *
  479. * @returns {Promise} - returns promise (reject, resolve)
  480. */
  481. GET_ORPHANED_PLAYLIST_SONGS() {
  482. return new Promise((resolve, reject) => {
  483. DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this).then(playlistModel => {
  484. playlistModel.find({}, (err, playlists) => {
  485. if (err) reject(new Error(err));
  486. else {
  487. SongsModule.SongModel.find({}, { _id: true, songId: true }, (err, songs) => {
  488. if (err) reject(new Error(err));
  489. else {
  490. const musareSongIds = songs.map(song => song._id.toString());
  491. const orphanedSongIds = new Set();
  492. async.eachLimit(
  493. playlists,
  494. 1,
  495. (playlist, next) => {
  496. playlist.songs.forEach(song => {
  497. if (
  498. (!song._id || musareSongIds.indexOf(song._id.toString() === -1)) &&
  499. !orphanedSongIds.has(song.songId)
  500. ) {
  501. orphanedSongIds.add(song.songId);
  502. }
  503. });
  504. next();
  505. },
  506. () => {
  507. resolve({ songIds: Array.from(orphanedSongIds) });
  508. }
  509. );
  510. }
  511. });
  512. }
  513. });
  514. });
  515. });
  516. }
  517. /**
  518. * Requests a song, adding it to the DB
  519. *
  520. * @param {object} payload - The payload
  521. * @param {string} payload.songId - The YouTube song id of the song
  522. * @param {string} payload.userId - The user id of the person requesting the song
  523. * @returns {Promise} - returns promise (reject, resolve)
  524. */
  525. REQUEST_SONG(payload) {
  526. return new Promise((resolve, reject) => {
  527. const { songId, userId } = payload;
  528. const requestedAt = Date.now();
  529. async.waterfall(
  530. [
  531. next => {
  532. SongsModule.SongModel.findOne({ songId }, next);
  533. },
  534. // Get YouTube data from id
  535. (song, next) => {
  536. if (song) return next("This song is already in the database.");
  537. // TODO Add err object as first param of callback
  538. return YouTubeModule.runJob("GET_SONG", { songId }, this)
  539. .then(response => {
  540. const { song } = response;
  541. song.artists = [];
  542. song.genres = [];
  543. song.skipDuration = 0;
  544. song.explicit = false;
  545. song.requestedBy = userId;
  546. song.requestedAt = requestedAt;
  547. song.status = "unverified";
  548. next(null, song);
  549. })
  550. .catch(next);
  551. },
  552. (newSong, next) => {
  553. const song = new SongsModule.SongModel(newSong);
  554. song.save({ validateBeforeSave: false }, err => {
  555. if (err) return next(err, song);
  556. return next(null, song);
  557. });
  558. },
  559. (song, next) => {
  560. DBModule.runJob("GET_MODEL", { modelName: "user" }, this)
  561. .then(UserModel => {
  562. UserModel.findOne({ _id: userId }, (err, user) => {
  563. if (err) return next(err);
  564. if (!user) return next(null, song);
  565. user.statistics.songsRequested += 1;
  566. return user.save(err => {
  567. if (err) return next(err);
  568. return next(null, song);
  569. });
  570. });
  571. })
  572. .catch(next);
  573. }
  574. ],
  575. async (err, song) => {
  576. if (err) reject(err);
  577. SongsModule.runJob("UPDATE_SONG", { songId });
  578. CacheModule.runJob("PUB", {
  579. channel: "song.newUnverifiedSong",
  580. value: song._id
  581. });
  582. resolve();
  583. }
  584. );
  585. });
  586. }
  587. /**
  588. * Hides a song
  589. *
  590. * @param {object} payload - The payload
  591. * @param {string} payload.songId - The Musare song id of the song
  592. * @returns {Promise} - returns promise (reject, resolve)
  593. */
  594. HIDE_SONG(payload) {
  595. return new Promise((resolve, reject) => {
  596. const { songId } = payload;
  597. async.waterfall(
  598. [
  599. next => {
  600. SongsModule.SongModel.findOne({ _id: songId }, next);
  601. },
  602. // Get YouTube data from id
  603. (song, next) => {
  604. if (!song) return next("This song does not exist.");
  605. if (song.status === "hidden") return next("This song is already hidden.");
  606. if (song.status === "verified") return next("Verified songs cannot be hidden.");
  607. // TODO Add err object as first param of callback
  608. return next();
  609. },
  610. next => {
  611. SongsModule.SongModel.updateOne({ _id: songId }, { status: "hidden" }, next);
  612. },
  613. (res, next) => {
  614. SongsModule.runJob("UPDATE_SONG", { songId });
  615. next();
  616. }
  617. ],
  618. async err => {
  619. if (err) reject(err);
  620. CacheModule.runJob("PUB", {
  621. channel: "song.newHiddenSong",
  622. value: songId
  623. });
  624. CacheModule.runJob("PUB", {
  625. channel: "song.removedUnverifiedSong",
  626. value: songId
  627. });
  628. resolve();
  629. }
  630. );
  631. });
  632. }
  633. /**
  634. * Unhides a song
  635. *
  636. * @param {object} payload - The payload
  637. * @param {string} payload.songId - The Musare song id of the song
  638. * @returns {Promise} - returns promise (reject, resolve)
  639. */
  640. UNHIDE_SONG(payload) {
  641. return new Promise((resolve, reject) => {
  642. const { songId } = payload;
  643. async.waterfall(
  644. [
  645. next => {
  646. SongsModule.SongModel.findOne({ _id: songId }, next);
  647. },
  648. // Get YouTube data from id
  649. (song, next) => {
  650. if (!song) return next("This song does not exist.");
  651. if (song.status !== "hidden") return next("This song is not hidden.");
  652. // TODO Add err object as first param of callback
  653. return next();
  654. },
  655. next => {
  656. SongsModule.SongModel.updateOne({ _id: songId }, { status: "unverified" }, next);
  657. },
  658. (res, next) => {
  659. SongsModule.runJob("UPDATE_SONG", { songId });
  660. next();
  661. }
  662. ],
  663. async err => {
  664. if (err) reject(err);
  665. CacheModule.runJob("PUB", {
  666. channel: "song.newUnverifiedSong",
  667. value: songId
  668. });
  669. CacheModule.runJob("PUB", {
  670. channel: "song.removedHiddenSong",
  671. value: songId
  672. });
  673. resolve();
  674. }
  675. );
  676. });
  677. }
  678. // runjob songs REQUEST_ORPHANED_PLAYLIST_SONGS {}
  679. /**
  680. * Requests all orphaned playlist songs, adding them to the database
  681. *
  682. * @returns {Promise} - returns promise (reject, resolve)
  683. */
  684. REQUEST_ORPHANED_PLAYLIST_SONGS() {
  685. return new Promise((resolve, reject) => {
  686. DBModule.runJob("GET_MODEL", { modelName: "playlist" })
  687. .then(playlistModel => {
  688. SongsModule.runJob("GET_ORPHANED_PLAYLIST_SONGS", {}, this).then(response => {
  689. const { songIds } = response;
  690. const playlistsToUpdate = new Set();
  691. async.eachLimit(
  692. songIds,
  693. 1,
  694. (songId, next) => {
  695. async.waterfall(
  696. [
  697. next => {
  698. console.log(
  699. songId,
  700. `this is song ${songIds.indexOf(songId) + 1}/${songIds.length}`
  701. );
  702. setTimeout(next, 150);
  703. },
  704. next => {
  705. SongsModule.runJob("ENSURE_SONG_EXISTS_BY_SONG_ID", { songId }, this)
  706. .then(() => next())
  707. .catch(next);
  708. // SongsModule.runJob("REQUEST_SONG", { songId, userId: null }, this)
  709. // .then(() => {
  710. // next();
  711. // })
  712. // .catch(next);
  713. },
  714. next => {
  715. console.log(444, songId);
  716. SongsModule.SongModel.findOne({ songId }, next);
  717. },
  718. (song, next) => {
  719. const { _id, title, artists, thumbnail, duration, status } = song;
  720. const trimmedSong = {
  721. _id,
  722. songId,
  723. title,
  724. artists,
  725. thumbnail,
  726. duration,
  727. status
  728. };
  729. playlistModel.updateMany(
  730. { "songs.songId": song.songId },
  731. { $set: { "songs.$": trimmedSong } },
  732. err => {
  733. next(err, song);
  734. }
  735. );
  736. },
  737. (song, next) => {
  738. playlistModel.find({ "songs._id": song._id }, next);
  739. },
  740. (playlists, next) => {
  741. playlists.forEach(playlist => {
  742. playlistsToUpdate.add(playlist._id.toString());
  743. });
  744. next();
  745. }
  746. ],
  747. next
  748. );
  749. },
  750. err => {
  751. if (err) reject(err);
  752. else {
  753. async.eachLimit(
  754. Array.from(playlistsToUpdate),
  755. 1,
  756. (playlistId, next) => {
  757. PlaylistsModule.runJob(
  758. "UPDATE_PLAYLIST",
  759. {
  760. playlistId
  761. },
  762. this
  763. )
  764. .then(() => {
  765. next();
  766. })
  767. .catch(next);
  768. },
  769. err => {
  770. if (err) reject(err);
  771. else resolve();
  772. }
  773. );
  774. }
  775. }
  776. );
  777. });
  778. })
  779. .catch(reject);
  780. });
  781. }
  782. }
  783. export default new _SongsModule();