playlists.js 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524
  1. import async from "async";
  2. import CoreClass from "../core";
  3. let PlaylistsModule;
  4. let StationsModule;
  5. let SongsModule;
  6. let CacheModule;
  7. let DBModule;
  8. let UtilsModule;
  9. class _PlaylistsModule extends CoreClass {
  10. // eslint-disable-next-line require-jsdoc
  11. constructor() {
  12. super("playlists");
  13. PlaylistsModule = this;
  14. }
  15. /**
  16. * Initialises the playlists module
  17. *
  18. * @returns {Promise} - returns promise (reject, resolve)
  19. */
  20. async initialize() {
  21. this.setStage(1);
  22. StationsModule = this.moduleManager.modules.stations;
  23. CacheModule = this.moduleManager.modules.cache;
  24. DBModule = this.moduleManager.modules.db;
  25. UtilsModule = this.moduleManager.modules.utils;
  26. SongsModule = this.moduleManager.modules.songs;
  27. this.playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" });
  28. this.playlistSchemaCache = await CacheModule.runJob("GET_SCHEMA", { schemaName: "playlist" });
  29. this.setStage(2);
  30. return new Promise((resolve, reject) => {
  31. async.waterfall(
  32. [
  33. next => {
  34. this.setStage(3);
  35. CacheModule.runJob("HGETALL", { table: "playlists" })
  36. .then(playlists => {
  37. next(null, playlists);
  38. })
  39. .catch(next);
  40. },
  41. (playlists, next) => {
  42. this.setStage(4);
  43. if (!playlists) return next();
  44. const playlistIds = Object.keys(playlists);
  45. return async.each(
  46. playlistIds,
  47. (playlistId, next) => {
  48. PlaylistsModule.playlistModel.findOne({ _id: playlistId }, (err, playlist) => {
  49. if (err) next(err);
  50. else if (!playlist) {
  51. CacheModule.runJob("HDEL", {
  52. table: "playlists",
  53. key: playlistId
  54. })
  55. .then(() => next())
  56. .catch(next);
  57. } else next();
  58. });
  59. },
  60. next
  61. );
  62. },
  63. next => {
  64. this.setStage(5);
  65. PlaylistsModule.playlistModel.find({}, next);
  66. },
  67. (playlists, next) => {
  68. this.setStage(6);
  69. async.each(
  70. playlists,
  71. (playlist, cb) => {
  72. CacheModule.runJob("HSET", {
  73. table: "playlists",
  74. key: playlist._id,
  75. value: PlaylistsModule.playlistSchemaCache(playlist)
  76. })
  77. .then(() => cb())
  78. .catch(next);
  79. },
  80. next
  81. );
  82. }
  83. ],
  84. async err => {
  85. if (err) {
  86. const formattedErr = await UtilsModule.runJob("GET_ERROR", {
  87. error: err
  88. });
  89. reject(new Error(formattedErr));
  90. } else {
  91. resolve();
  92. }
  93. }
  94. );
  95. });
  96. }
  97. /**
  98. * Returns a list of playlists that include a specific song
  99. *
  100. * @param {object} payload - object that contains the payload
  101. * @param {string} payload.songId - the song id
  102. * @param {string} payload.includeSongs - include the songs
  103. * @returns {Promise} - returns promise (reject, resolve)
  104. */
  105. GET_PLAYLISTS_WITH_SONG(payload) {
  106. return new Promise((resolve, reject) => {
  107. const includeObject = payload.includeSongs ? null : { songs: false };
  108. PlaylistsModule.playlistModel.find({ "songs._id": payload.songId }, includeObject, (err, playlists) => {
  109. if (err) reject(err);
  110. else resolve({ playlists });
  111. });
  112. });
  113. }
  114. /**
  115. * Creates a playlist owned by a user
  116. *
  117. * @param {object} payload - object that contains the payload
  118. * @param {string} payload.userId - the id of the user to create the playlist for
  119. * @param {string} payload.displayName - the display name of the playlist
  120. * @param {string} payload.type - the type of the playlist
  121. * @returns {Promise} - returns promise (reject, resolve)
  122. */
  123. CREATE_USER_PLAYLIST(payload) {
  124. return new Promise((resolve, reject) => {
  125. PlaylistsModule.playlistModel.create(
  126. {
  127. displayName: payload.displayName,
  128. songs: [],
  129. createdBy: payload.userId,
  130. createdAt: Date.now(),
  131. createdFor: null,
  132. type: payload.type
  133. },
  134. (err, playlist) => {
  135. if (err) return reject(new Error(err));
  136. return resolve(playlist._id);
  137. }
  138. );
  139. });
  140. }
  141. /**
  142. * Creates a playlist that contains all songs of a specific genre
  143. *
  144. * @param {object} payload - object that contains the payload
  145. * @param {string} payload.genre - the genre
  146. * @returns {Promise} - returns promise (reject, resolve)
  147. */
  148. CREATE_GENRE_PLAYLIST(payload) {
  149. return new Promise((resolve, reject) => {
  150. PlaylistsModule.runJob("GET_GENRE_PLAYLIST", { genre: payload.genre.toLowerCase() }, this)
  151. .then(() => {
  152. reject(new Error("Playlist already exists"));
  153. })
  154. .catch(err => {
  155. if (err.message === "Playlist not found") {
  156. PlaylistsModule.playlistModel.create(
  157. {
  158. displayName: `Genre - ${payload.genre}`,
  159. songs: [],
  160. createdBy: "Musare",
  161. createdFor: `${payload.genre.toLowerCase()}`,
  162. createdAt: Date.now(),
  163. type: "genre"
  164. },
  165. (err, playlist) => {
  166. if (err) return reject(new Error(err));
  167. return resolve(playlist._id);
  168. }
  169. );
  170. } else reject(new Error(err));
  171. });
  172. });
  173. }
  174. /**
  175. * Creates a playlist that contains all songs of a specific artist
  176. *
  177. * @param {object} payload - object that contains the payload
  178. * @param {string} payload.artist - the artist
  179. * @returns {Promise} - returns promise (reject, resolve)
  180. */
  181. CREATE_ARTIST_PLAYLIST(payload) {
  182. return new Promise((resolve, reject) => {
  183. PlaylistsModule.runJob("GET_ARTIST_PLAYLIST", { artist: payload.artist.toLowerCase() }, this)
  184. .then(() => {
  185. reject(new Error("Playlist already exists"));
  186. })
  187. .catch(err => {
  188. if (err.message === "Playlist not found") {
  189. PlaylistsModule.playlistModel.create(
  190. {
  191. isUserModifiable: false,
  192. displayName: `Artist - ${payload.artist}`,
  193. songs: [],
  194. createdBy: "Musare",
  195. createdFor: `${payload.artist.toLowerCase()}`,
  196. createdAt: Date.now(),
  197. type: "artist",
  198. privacy: "public"
  199. },
  200. (err, playlist) => {
  201. if (err) return reject(new Error(err));
  202. return resolve(playlist._id);
  203. }
  204. );
  205. } else reject(new Error(err));
  206. });
  207. });
  208. }
  209. /**
  210. * Gets all genre playlists
  211. *
  212. * @param {object} payload - object that contains the payload
  213. * @param {string} payload.includeSongs - include the songs
  214. * @returns {Promise} - returns promise (reject, resolve)
  215. */
  216. GET_ALL_GENRE_PLAYLISTS(payload) {
  217. return new Promise((resolve, reject) => {
  218. const includeObject = payload.includeSongs ? null : { songs: false };
  219. PlaylistsModule.playlistModel.find({ type: "genre" }, includeObject, (err, playlists) => {
  220. if (err) reject(new Error(err));
  221. else resolve({ playlists });
  222. });
  223. });
  224. }
  225. /**
  226. * Gets all artist playlists
  227. *
  228. * @param {object} payload - object that contains the payload
  229. * @param {string} payload.includeSongs - include the songs
  230. * @returns {Promise} - returns promise (reject, resolve)
  231. */
  232. GET_ALL_ARTIST_PLAYLISTS(payload) {
  233. return new Promise((resolve, reject) => {
  234. const includeObject = payload.includeSongs ? null : { songs: false };
  235. PlaylistsModule.playlistModel.find({ type: "artist" }, includeObject, (err, playlists) => {
  236. if (err) reject(new Error(err));
  237. else resolve({ playlists });
  238. });
  239. });
  240. }
  241. /**
  242. * Gets all station playlists
  243. *
  244. * @param {object} payload - object that contains the payload
  245. * @param {string} payload.includeSongs - include the songs
  246. * @returns {Promise} - returns promise (reject, resolve)
  247. */
  248. GET_ALL_STATION_PLAYLISTS(payload) {
  249. return new Promise((resolve, reject) => {
  250. const includeObject = payload.includeSongs ? null : { songs: false };
  251. PlaylistsModule.playlistModel.find({ type: "station" }, includeObject, (err, playlists) => {
  252. if (err) reject(new Error(err));
  253. else resolve({ playlists });
  254. });
  255. });
  256. }
  257. /**
  258. * Gets a genre playlist
  259. *
  260. * @param {object} payload - object that contains the payload
  261. * @param {string} payload.genre - the genre
  262. * @param {string} payload.includeSongs - include the songs
  263. * @returns {Promise} - returns promise (reject, resolve)
  264. */
  265. GET_GENRE_PLAYLIST(payload) {
  266. return new Promise((resolve, reject) => {
  267. const includeObject = payload.includeSongs ? null : { songs: false };
  268. PlaylistsModule.playlistModel.findOne(
  269. { type: "genre", createdFor: payload.genre },
  270. includeObject,
  271. (err, playlist) => {
  272. if (err) reject(new Error(err));
  273. else if (!playlist) reject(new Error("Playlist not found"));
  274. else resolve({ playlist });
  275. }
  276. );
  277. });
  278. }
  279. /**
  280. * Gets all missing genre playlists
  281. *
  282. * @returns {Promise} - returns promise (reject, resolve)
  283. */
  284. GET_MISSING_GENRE_PLAYLISTS() {
  285. return new Promise((resolve, reject) => {
  286. SongsModule.runJob("GET_ALL_GENRES", {}, this)
  287. .then(response => {
  288. const { genres } = response;
  289. const missingGenres = [];
  290. async.eachLimit(
  291. genres,
  292. 1,
  293. (genre, next) => {
  294. PlaylistsModule.runJob(
  295. "GET_GENRE_PLAYLIST",
  296. { genre: genre.toLowerCase(), includeSongs: false },
  297. this
  298. )
  299. .then(() => {
  300. next();
  301. })
  302. .catch(err => {
  303. if (err.message === "Playlist not found") {
  304. missingGenres.push(genre);
  305. next();
  306. } else next(err);
  307. });
  308. },
  309. err => {
  310. if (err) reject(err);
  311. else resolve({ genres: missingGenres });
  312. }
  313. );
  314. })
  315. .catch(err => {
  316. reject(err);
  317. });
  318. });
  319. }
  320. /**
  321. * Creates all missing genre playlists
  322. *
  323. * @returns {Promise} - returns promise (reject, resolve)
  324. */
  325. CREATE_MISSING_GENRE_PLAYLISTS() {
  326. return new Promise((resolve, reject) => {
  327. PlaylistsModule.runJob("GET_MISSING_GENRE_PLAYLISTS", {}, this)
  328. .then(response => {
  329. const { genres } = response;
  330. async.eachLimit(
  331. genres,
  332. 1,
  333. (genre, next) => {
  334. PlaylistsModule.runJob("CREATE_GENRE_PLAYLIST", { genre }, this)
  335. .then(() => {
  336. next();
  337. })
  338. .catch(err => {
  339. next(err);
  340. });
  341. },
  342. err => {
  343. if (err) reject(err);
  344. else resolve();
  345. }
  346. );
  347. })
  348. .catch(err => {
  349. reject(err);
  350. });
  351. });
  352. }
  353. /**
  354. * Gets a artist playlist
  355. *
  356. * @param {object} payload - object that contains the payload
  357. * @param {string} payload.artist - the artist
  358. * @param {string} payload.includeSongs - include the songs
  359. * @returns {Promise} - returns promise (reject, resolve)
  360. */
  361. GET_ARTIST_PLAYLIST(payload) {
  362. return new Promise((resolve, reject) => {
  363. const includeObject = payload.includeSongs ? null : { songs: false };
  364. PlaylistsModule.playlistModel.findOne(
  365. { type: "artist", createdFor: payload.artist },
  366. includeObject,
  367. (err, playlist) => {
  368. if (err) reject(new Error(err));
  369. else if (!playlist) reject(new Error("Playlist not found"));
  370. else resolve({ playlist });
  371. }
  372. );
  373. });
  374. }
  375. /**
  376. * Gets all missing artist playlists
  377. *
  378. * @returns {Promise} - returns promise (reject, resolve)
  379. */
  380. GET_MISSING_ARTIST_PLAYLISTS() {
  381. return new Promise((resolve, reject) => {
  382. SongsModule.runJob("GET_ALL_ARTISTS", {}, this)
  383. .then(response => {
  384. const { artists } = response;
  385. const missingArtists = [];
  386. async.eachLimit(
  387. artists,
  388. 1,
  389. (artist, next) => {
  390. PlaylistsModule.runJob(
  391. "GET_ARTIST_PLAYLIST",
  392. { artist: artist.toLowerCase(), includeSongs: false },
  393. this
  394. )
  395. .then(() => {
  396. next();
  397. })
  398. .catch(err => {
  399. if (err.message === "Playlist not found") {
  400. missingArtists.push(artist);
  401. next();
  402. } else next(err);
  403. });
  404. },
  405. err => {
  406. if (err) reject(err);
  407. else resolve({ artists: missingArtists });
  408. }
  409. );
  410. })
  411. .catch(err => {
  412. reject(err);
  413. });
  414. });
  415. }
  416. /**
  417. * Creates all missing artist playlists
  418. *
  419. * @returns {Promise} - returns promise (reject, resolve)
  420. */
  421. CREATE_MISSING_ARTIST_PLAYLISTS() {
  422. return new Promise((resolve, reject) => {
  423. PlaylistsModule.runJob("GET_MISSING_ARTIST_PLAYLISTS", {}, this)
  424. .then(response => {
  425. const { artists } = response;
  426. async.eachLimit(
  427. artists,
  428. 1,
  429. (artist, next) => {
  430. PlaylistsModule.runJob("CREATE_ARTIST_PLAYLIST", { artist }, this)
  431. .then(() => {
  432. next();
  433. })
  434. .catch(err => {
  435. next(err);
  436. });
  437. },
  438. err => {
  439. if (err) reject(err);
  440. else resolve();
  441. }
  442. );
  443. })
  444. .catch(err => {
  445. reject(err);
  446. });
  447. });
  448. }
  449. /**
  450. * Gets a station playlist
  451. *
  452. * @param {object} payload - object that contains the payload
  453. * @param {string} payload.staationId - the station id
  454. * @param {string} payload.includeSongs - include the songs
  455. * @returns {Promise} - returns promise (reject, resolve)
  456. */
  457. GET_STATION_PLAYLIST(payload) {
  458. return new Promise((resolve, reject) => {
  459. const includeObject = payload.includeSongs ? null : { songs: false };
  460. PlaylistsModule.playlistModel.findOne(
  461. { type: "station", createdFor: payload.stationId },
  462. includeObject,
  463. (err, playlist) => {
  464. if (err) reject(new Error(err));
  465. else if (!playlist) reject(new Error("Playlist not found"));
  466. else resolve({ playlist });
  467. }
  468. );
  469. });
  470. }
  471. /**
  472. * Adds a song to a playlist
  473. *
  474. * @param {object} payload - object that contains the payload
  475. * @param {string} payload.playlistId - the playlist id
  476. * @param {string} payload.song - the song
  477. * @returns {Promise} - returns promise (reject, resolve)
  478. */
  479. ADD_SONG_TO_PLAYLIST(payload) {
  480. return new Promise((resolve, reject) => {
  481. const { _id, youtubeId, title, artists, thumbnail, duration, verified } = payload.song;
  482. const trimmedSong = {
  483. _id,
  484. youtubeId,
  485. title,
  486. artists,
  487. thumbnail,
  488. duration,
  489. verified
  490. };
  491. PlaylistsModule.playlistModel.updateOne(
  492. { _id: payload.playlistId },
  493. { $push: { songs: trimmedSong } },
  494. { runValidators: true },
  495. err => {
  496. if (err) reject(new Error(err));
  497. else {
  498. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId: payload.playlistId }, this)
  499. .then(() => resolve())
  500. .catch(err => {
  501. reject(new Error(err));
  502. });
  503. }
  504. }
  505. );
  506. });
  507. }
  508. /**
  509. * Deletes a song from a playlist based on the youtube id
  510. *
  511. * @param {object} payload - object that contains the payload
  512. * @param {string} payload.playlistId - the playlist id
  513. * @param {string} payload.youtubeId - the youtube id
  514. * @returns {Promise} - returns promise (reject, resolve)
  515. */
  516. DELETE_SONG_FROM_PLAYLIST_BY_YOUTUBE_ID(payload) {
  517. return new Promise((resolve, reject) => {
  518. PlaylistsModule.playlistModel.updateOne(
  519. { _id: payload.playlistId },
  520. { $pull: { songs: { youtubeId: payload.youtubeId } } },
  521. err => {
  522. if (err) reject(new Error(err));
  523. else {
  524. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId: payload.playlistId }, this)
  525. .then(() => resolve())
  526. .catch(err => {
  527. reject(new Error(err));
  528. });
  529. }
  530. }
  531. );
  532. });
  533. }
  534. /**
  535. * Fills a genre playlist with songs
  536. *
  537. * @param {object} payload - object that contains the payload
  538. * @param {string} payload.genre - the genre
  539. * @param {string} payload.createPlaylist - create playlist if it doesn't exist, default false
  540. * @returns {Promise} - returns promise (reject, resolve)
  541. */
  542. AUTOFILL_GENRE_PLAYLIST(payload) {
  543. return new Promise((resolve, reject) => {
  544. async.waterfall(
  545. [
  546. next => {
  547. PlaylistsModule.runJob(
  548. "GET_GENRE_PLAYLIST",
  549. { genre: payload.genre.toLowerCase(), includeSongs: true },
  550. this
  551. )
  552. .then(response => {
  553. next(null, response.playlist._id);
  554. })
  555. .catch(err => {
  556. if (err.message === "Playlist not found") {
  557. if (payload.createPlaylist)
  558. PlaylistsModule.runJob("CREATE_GENRE_PLAYLIST", { genre: payload.genre }, this)
  559. .then(playlistId => {
  560. next(null, playlistId);
  561. })
  562. .catch(err => {
  563. next(err);
  564. });
  565. } else next(err);
  566. });
  567. },
  568. (playlistId, next) => {
  569. SongsModule.runJob("GET_ALL_SONGS_WITH_GENRE", { genre: payload.genre }, this)
  570. .then(response => {
  571. next(null, playlistId, response.songs);
  572. })
  573. .catch(err => {
  574. console.log(err);
  575. next(err);
  576. });
  577. },
  578. (playlistId, _songs, next) => {
  579. const songs = _songs.map(song => {
  580. const { _id, youtubeId, title, artists, thumbnail, duration, verified } = song;
  581. return {
  582. _id,
  583. youtubeId,
  584. title,
  585. artists,
  586. thumbnail,
  587. duration,
  588. verified
  589. };
  590. });
  591. PlaylistsModule.playlistModel.updateOne({ _id: playlistId }, { $set: { songs } }, err => {
  592. next(err, playlistId);
  593. });
  594. },
  595. (playlistId, next) => {
  596. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  597. .then(() => {
  598. next(null, playlistId);
  599. })
  600. .catch(next);
  601. },
  602. (playlistId, next) => {
  603. StationsModule.runJob("GET_STATIONS_THAT_AUTOFILL_OR_BLACKLIST_PLAYLIST", { playlistId }, this)
  604. .then(response => {
  605. async.eachLimit(
  606. response.stationIds,
  607. 1,
  608. (stationId, next) => {
  609. PlaylistsModule.runJob("AUTOFILL_STATION_PLAYLIST", { stationId }, this)
  610. .then(() => {
  611. next();
  612. })
  613. .catch(err => {
  614. next(err);
  615. });
  616. },
  617. err => {
  618. if (err) next(err);
  619. else next();
  620. }
  621. );
  622. })
  623. .catch(err => {
  624. next(err);
  625. });
  626. }
  627. ],
  628. err => {
  629. if (err && err !== true) return reject(new Error(err));
  630. return resolve({});
  631. }
  632. );
  633. });
  634. }
  635. /**
  636. * Gets orphaned genre playlists
  637. *
  638. * @returns {Promise} - returns promise (reject, resolve)
  639. */
  640. GET_ORPHANED_GENRE_PLAYLISTS() {
  641. return new Promise((resolve, reject) => {
  642. PlaylistsModule.playlistModel.find({ type: "genre" }, { songs: false }, (err, playlists) => {
  643. if (err) reject(new Error(err));
  644. else {
  645. const orphanedPlaylists = [];
  646. async.eachLimit(
  647. playlists,
  648. 1,
  649. (playlist, next) => {
  650. SongsModule.runJob("GET_ALL_SONGS_WITH_GENRE", { genre: playlist.createdFor }, this)
  651. .then(response => {
  652. if (response.songs.length === 0) {
  653. StationsModule.runJob(
  654. "GET_STATIONS_THAT_AUTOFILL_OR_BLACKLIST_PLAYLIST",
  655. { playlistId: playlist._id },
  656. this
  657. )
  658. .then(response => {
  659. if (response.stationIds.length === 0) orphanedPlaylists.push(playlist);
  660. next();
  661. })
  662. .catch(next);
  663. } else next();
  664. })
  665. .catch(next);
  666. },
  667. err => {
  668. if (err) reject(new Error(err));
  669. else resolve({ playlists: orphanedPlaylists });
  670. }
  671. );
  672. }
  673. });
  674. });
  675. }
  676. /**
  677. * Deletes all orphaned genre playlists
  678. *
  679. * @returns {Promise} - returns promise (reject, resolve)
  680. */
  681. DELETE_ORPHANED_GENRE_PLAYLISTS() {
  682. return new Promise((resolve, reject) => {
  683. PlaylistsModule.runJob("GET_ORPHANED_GENRE_PLAYLISTS", {}, this)
  684. .then(response => {
  685. async.eachLimit(
  686. response.playlists,
  687. 1,
  688. (playlist, next) => {
  689. PlaylistsModule.runJob("DELETE_PLAYLIST", { playlistId: playlist._id }, this)
  690. .then(() => {
  691. this.log("INFO", "Deleting orphaned genre playlist");
  692. next();
  693. })
  694. .catch(err => {
  695. next(err);
  696. });
  697. },
  698. err => {
  699. if (err) reject(new Error(err));
  700. else resolve({});
  701. }
  702. );
  703. })
  704. .catch(err => {
  705. reject(new Error(err));
  706. });
  707. });
  708. }
  709. /**
  710. * Fills a artist playlist with songs
  711. *
  712. * @param {object} payload - object that contains the payload
  713. * @param {string} payload.artist - the artist
  714. * @returns {Promise} - returns promise (reject, resolve)
  715. */
  716. AUTOFILL_ARTIST_PLAYLIST(payload) {
  717. return new Promise((resolve, reject) => {
  718. async.waterfall(
  719. [
  720. next => {
  721. PlaylistsModule.runJob(
  722. "GET_ARTIST_PLAYLIST",
  723. { artist: payload.artist.toLowerCase(), includeSongs: true },
  724. this
  725. )
  726. .then(response => {
  727. next(null, response.playlist._id);
  728. })
  729. .catch(err => {
  730. if (err.message === "Playlist not found") {
  731. PlaylistsModule.runJob("CREATE_ARTIST_PLAYLIST", { artist: payload.artist }, this)
  732. .then(playlistId => {
  733. next(null, playlistId);
  734. })
  735. .catch(err => {
  736. next(err);
  737. });
  738. } else next(err);
  739. });
  740. },
  741. (playlistId, next) => {
  742. SongsModule.runJob("GET_ALL_SONGS_WITH_ARTIST", { artist: payload.artist }, this)
  743. .then(response => {
  744. next(null, playlistId, response.songs);
  745. })
  746. .catch(err => {
  747. console.log(err);
  748. next(err);
  749. });
  750. },
  751. (playlistId, _songs, next) => {
  752. const songs = _songs.map(song => {
  753. const { _id, youtubeId, title, artists, thumbnail, duration, status } = song;
  754. return {
  755. _id,
  756. youtubeId,
  757. title,
  758. artists,
  759. thumbnail,
  760. duration,
  761. status
  762. };
  763. });
  764. PlaylistsModule.playlistModel.updateOne({ _id: playlistId }, { $set: { songs } }, err => {
  765. next(err, playlistId);
  766. });
  767. },
  768. (playlistId, next) => {
  769. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  770. .then(() => {
  771. next(null, playlistId);
  772. })
  773. .catch(next);
  774. },
  775. (playlistId, next) => {
  776. StationsModule.runJob("GET_STATIONS_THAT_INCLUDE_OR_EXCLUDE_PLAYLIST", { playlistId }, this)
  777. .then(response => {
  778. async.eachLimit(
  779. response.stationIds,
  780. 1,
  781. (stationId, next) => {
  782. PlaylistsModule.runJob("AUTOFILL_STATION_PLAYLIST", { stationId }, this)
  783. .then(() => {
  784. next();
  785. })
  786. .catch(err => {
  787. next(err);
  788. });
  789. },
  790. err => {
  791. if (err) next(err);
  792. else next();
  793. }
  794. );
  795. })
  796. .catch(err => {
  797. next(err);
  798. });
  799. }
  800. ],
  801. err => {
  802. if (err && err !== true) return reject(new Error(err));
  803. return resolve({});
  804. }
  805. );
  806. });
  807. }
  808. /**
  809. * Gets orphaned artist playlists
  810. *
  811. * @returns {Promise} - returns promise (reject, resolve)
  812. */
  813. GET_ORPHANED_ARTIST_PLAYLISTS() {
  814. return new Promise((resolve, reject) => {
  815. PlaylistsModule.playlistModel.find({ type: "artist" }, { songs: false }, (err, playlists) => {
  816. if (err) reject(new Error(err));
  817. else {
  818. const orphanedPlaylists = [];
  819. async.eachLimit(
  820. playlists,
  821. 1,
  822. (playlist, next) => {
  823. SongsModule.runJob("GET_ALL_SONGS_WITH_ARTIST", { artist: playlist.createdFor }, this)
  824. .then(response => {
  825. if (response.songs.length === 0) {
  826. StationsModule.runJob(
  827. "GET_STATIONS_THAT_INCLUDE_OR_EXCLUDE_PLAYLIST",
  828. { playlistId: playlist._id },
  829. this
  830. )
  831. .then(response => {
  832. if (response.stationIds.length === 0) orphanedPlaylists.push(playlist);
  833. next();
  834. })
  835. .catch(next);
  836. } else next();
  837. })
  838. .catch(next);
  839. },
  840. err => {
  841. if (err) reject(new Error(err));
  842. else resolve({ playlists: orphanedPlaylists });
  843. }
  844. );
  845. }
  846. });
  847. });
  848. }
  849. /**
  850. * Deletes all orphaned artist playlists
  851. *
  852. * @returns {Promise} - returns promise (reject, resolve)
  853. */
  854. DELETE_ORPHANED_ARTIST_PLAYLISTS() {
  855. return new Promise((resolve, reject) => {
  856. PlaylistsModule.runJob("GET_ORPHANED_ARTIST_PLAYLISTS", {}, this)
  857. .then(response => {
  858. async.eachLimit(
  859. response.playlists,
  860. 1,
  861. (playlist, next) => {
  862. PlaylistsModule.runJob("DELETE_PLAYLIST", { playlistId: playlist._id }, this)
  863. .then(() => {
  864. this.log("INFO", "Deleting orphaned artist playlist");
  865. next();
  866. })
  867. .catch(err => {
  868. next(err);
  869. });
  870. },
  871. err => {
  872. if (err) reject(new Error(err));
  873. else resolve({});
  874. }
  875. );
  876. })
  877. .catch(err => {
  878. reject(new Error(err));
  879. });
  880. });
  881. }
  882. /**
  883. * Gets a orphaned station playlists
  884. *
  885. * @returns {Promise} - returns promise (reject, resolve)
  886. */
  887. GET_ORPHANED_STATION_PLAYLISTS() {
  888. return new Promise((resolve, reject) => {
  889. PlaylistsModule.playlistModel.find({ type: "station" }, { songs: false }, (err, playlists) => {
  890. if (err) reject(new Error(err));
  891. else {
  892. const orphanedPlaylists = [];
  893. async.eachLimit(
  894. playlists,
  895. 1,
  896. (playlist, next) => {
  897. StationsModule.runJob("GET_STATION", { stationId: playlist.createdFor }, this)
  898. .then(station => {
  899. if (station.playlist !== playlist._id.toString()) {
  900. orphanedPlaylists.push(playlist);
  901. }
  902. next();
  903. })
  904. .catch(err => {
  905. if (err.message === "Station not found") {
  906. orphanedPlaylists.push(playlist);
  907. next();
  908. } else next(err);
  909. });
  910. },
  911. err => {
  912. if (err) reject(new Error(err));
  913. else resolve({ playlists: orphanedPlaylists });
  914. }
  915. );
  916. }
  917. });
  918. });
  919. }
  920. /**
  921. * Deletes all orphaned station playlists
  922. *
  923. * @returns {Promise} - returns promise (reject, resolve)
  924. */
  925. DELETE_ORPHANED_STATION_PLAYLISTS() {
  926. return new Promise((resolve, reject) => {
  927. PlaylistsModule.runJob("GET_ORPHANED_STATION_PLAYLISTS", {}, this)
  928. .then(response => {
  929. async.eachLimit(
  930. response.playlists,
  931. 1,
  932. (playlist, next) => {
  933. PlaylistsModule.runJob("DELETE_PLAYLIST", { playlistId: playlist._id }, this)
  934. .then(() => {
  935. this.log("INFO", "Deleting orphaned station playlist");
  936. next();
  937. })
  938. .catch(err => {
  939. next(err);
  940. });
  941. },
  942. err => {
  943. if (err) reject(new Error(err));
  944. else resolve({});
  945. }
  946. );
  947. })
  948. .catch(err => {
  949. reject(new Error(err));
  950. });
  951. });
  952. }
  953. /**
  954. * Fills a station playlist with songs
  955. *
  956. * @param {object} payload - object that contains the payload
  957. * @param {string} payload.stationId - the station id
  958. * @returns {Promise} - returns promise (reject, resolve)
  959. */
  960. AUTOFILL_STATION_PLAYLIST(payload) {
  961. return new Promise((resolve, reject) => {
  962. let originalPlaylist = null;
  963. async.waterfall(
  964. [
  965. next => {
  966. if (!payload.stationId) next("Please specify a station id");
  967. else next();
  968. },
  969. next => {
  970. StationsModule.runJob("GET_STATION", { stationId: payload.stationId }, this)
  971. .then(station => {
  972. next(null, station);
  973. })
  974. .catch(next);
  975. },
  976. (station, next) => {
  977. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId: station.playlist }, this)
  978. .then(playlist => {
  979. originalPlaylist = playlist;
  980. next(null, station);
  981. })
  982. .catch(err => {
  983. next(err);
  984. });
  985. },
  986. (station, next) => {
  987. const playlists = [];
  988. async.eachLimit(
  989. station.autofill.playlists,
  990. 1,
  991. (playlistId, next) => {
  992. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  993. .then(playlist => {
  994. playlists.push(playlist);
  995. next();
  996. })
  997. .catch(next);
  998. },
  999. err => {
  1000. next(err, station, playlists);
  1001. }
  1002. );
  1003. },
  1004. (station, playlists, next) => {
  1005. const blacklist = [];
  1006. async.eachLimit(
  1007. station.blacklist,
  1008. 1,
  1009. (playlistId, next) => {
  1010. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  1011. .then(playlist => {
  1012. blacklist.push(playlist);
  1013. next();
  1014. })
  1015. .catch(next);
  1016. },
  1017. err => {
  1018. next(err, station, playlists, blacklist);
  1019. }
  1020. );
  1021. },
  1022. (station, playlists, blacklist, next) => {
  1023. const blacklistedSongs = blacklist
  1024. .flatMap(blacklistedPlaylist => blacklistedPlaylist.songs)
  1025. .reduce(
  1026. (items, item) =>
  1027. items.find(x => x.youtubeId === item.youtubeId) ? [...items] : [...items, item],
  1028. []
  1029. );
  1030. const includedSongs = playlists
  1031. .flatMap(playlist => playlist.songs)
  1032. .reduce(
  1033. (songs, song) =>
  1034. songs.find(x => x.youtubeId === song.youtubeId) ? [...songs] : [...songs, song],
  1035. []
  1036. )
  1037. .filter(song => !blacklistedSongs.find(x => x.youtubeId === song.youtubeId));
  1038. next(null, station, includedSongs);
  1039. },
  1040. (station, includedSongs, next) => {
  1041. PlaylistsModule.playlistModel.updateOne(
  1042. { _id: station.playlist },
  1043. { $set: { songs: includedSongs } },
  1044. err => {
  1045. next(err, includedSongs);
  1046. }
  1047. );
  1048. },
  1049. (includedSongs, next) => {
  1050. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId: originalPlaylist._id }, this)
  1051. .then(() => {
  1052. next(null, includedSongs);
  1053. })
  1054. .catch(next);
  1055. },
  1056. (includedSongs, next) => {
  1057. if (originalPlaylist.songs.length === 0 && includedSongs.length > 0)
  1058. StationsModule.runJob("SKIP_STATION", { stationId: payload.stationId, natural: false });
  1059. next();
  1060. }
  1061. ],
  1062. err => {
  1063. if (err && err !== true) return reject(new Error(err));
  1064. return resolve({});
  1065. }
  1066. );
  1067. });
  1068. }
  1069. /**
  1070. * Gets a playlist by id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
  1071. *
  1072. * @param {object} payload - object that contains the payload
  1073. * @param {string} payload.playlistId - the id of the playlist we are trying to get
  1074. * @returns {Promise} - returns promise (reject, resolve)
  1075. */
  1076. GET_PLAYLIST(payload) {
  1077. return new Promise((resolve, reject) => {
  1078. async.waterfall(
  1079. [
  1080. next => {
  1081. CacheModule.runJob(
  1082. "HGET",
  1083. {
  1084. table: "playlists",
  1085. key: payload.playlistId
  1086. },
  1087. this
  1088. )
  1089. .then(playlist => next(null, playlist))
  1090. .catch(next);
  1091. },
  1092. (playlist, next) => {
  1093. if (playlist)
  1094. PlaylistsModule.playlistModel.exists({ _id: payload.playlistId }, (err, exists) => {
  1095. if (err) next(err);
  1096. else if (exists) next(null, playlist);
  1097. else {
  1098. CacheModule.runJob(
  1099. "HDEL",
  1100. {
  1101. table: "playlists",
  1102. key: payload.playlistId
  1103. },
  1104. this
  1105. )
  1106. .then(() => next())
  1107. .catch(next);
  1108. }
  1109. });
  1110. else PlaylistsModule.playlistModel.findOne({ _id: payload.playlistId }, next);
  1111. },
  1112. (playlist, next) => {
  1113. if (playlist) {
  1114. CacheModule.runJob(
  1115. "HSET",
  1116. {
  1117. table: "playlists",
  1118. key: payload.playlistId,
  1119. value: playlist
  1120. },
  1121. this
  1122. )
  1123. .then(playlist => {
  1124. next(null, playlist);
  1125. })
  1126. .catch(next);
  1127. } else next("Playlist not found");
  1128. }
  1129. ],
  1130. (err, playlist) => {
  1131. if (err && err !== true) return reject(new Error(err));
  1132. return resolve(playlist);
  1133. }
  1134. );
  1135. });
  1136. }
  1137. /**
  1138. * Gets a playlist from id from Mongo and updates the cache with it
  1139. *
  1140. * @param {object} payload - object that contains the payload
  1141. * @param {string} payload.playlistId - the id of the playlist we are trying to update
  1142. * @returns {Promise} - returns promise (reject, resolve)
  1143. */
  1144. UPDATE_PLAYLIST(payload) {
  1145. return new Promise((resolve, reject) => {
  1146. async.waterfall(
  1147. [
  1148. next => {
  1149. PlaylistsModule.playlistModel.findOne({ _id: payload.playlistId }, next);
  1150. },
  1151. (playlist, next) => {
  1152. if (!playlist) {
  1153. CacheModule.runJob("HDEL", {
  1154. table: "playlists",
  1155. key: payload.playlistId
  1156. });
  1157. return next("Playlist not found");
  1158. }
  1159. return CacheModule.runJob(
  1160. "HSET",
  1161. {
  1162. table: "playlists",
  1163. key: payload.playlistId,
  1164. value: playlist
  1165. },
  1166. this
  1167. )
  1168. .then(playlist => {
  1169. next(null, playlist);
  1170. })
  1171. .catch(next);
  1172. }
  1173. ],
  1174. (err, playlist) => {
  1175. if (err && err !== true) return reject(new Error(err));
  1176. return resolve(playlist);
  1177. }
  1178. );
  1179. });
  1180. }
  1181. /**
  1182. * Deletes playlist from id from Mongo and cache
  1183. *
  1184. * @param {object} payload - object that contains the payload
  1185. * @param {string} payload.playlistId - the id of the playlist we are trying to delete
  1186. * @returns {Promise} - returns promise (reject, resolve)
  1187. */
  1188. DELETE_PLAYLIST(payload) {
  1189. return new Promise((resolve, reject) => {
  1190. async.waterfall(
  1191. [
  1192. next => {
  1193. PlaylistsModule.playlistModel.deleteOne({ _id: payload.playlistId }, next);
  1194. },
  1195. (res, next) => {
  1196. CacheModule.runJob(
  1197. "HDEL",
  1198. {
  1199. table: "playlists",
  1200. key: payload.playlistId
  1201. },
  1202. this
  1203. )
  1204. .then(() => next())
  1205. .catch(next);
  1206. },
  1207. next => {
  1208. StationsModule.runJob(
  1209. "REMOVE_AUTOFILLED_OR_BLACKLISTED_PLAYLIST_FROM_STATIONS",
  1210. { playlistId: payload.playlistId },
  1211. this
  1212. )
  1213. .then(() => {
  1214. next();
  1215. })
  1216. .catch(err => next(err));
  1217. }
  1218. ],
  1219. err => {
  1220. if (err && err !== true) return reject(new Error(err));
  1221. return resolve();
  1222. }
  1223. );
  1224. });
  1225. }
  1226. /**
  1227. * Searches through playlists
  1228. *
  1229. * @param {object} payload - object that contains the payload
  1230. * @param {string} payload.query - the query
  1231. * @param {string} payload.includePrivate - include private playlists
  1232. * @param {string} payload.includeStation - include station playlists
  1233. * @param {string} payload.includeUser - include user playlists
  1234. * @param {string} payload.includeGenre - include genre playlists
  1235. * @param {string} payload.includeArtist - include artist playlists
  1236. * @param {string} payload.includeOwn - include own user playlists
  1237. * @param {string} payload.userId - the user id of the person requesting
  1238. * @param {string} payload.includeSongs - include songs
  1239. * @param {string} payload.page - page (default 1)
  1240. * @returns {Promise} - returns promise (reject, resolve)
  1241. */
  1242. SEARCH(payload) {
  1243. return new Promise((resolve, reject) => {
  1244. async.waterfall(
  1245. [
  1246. next => {
  1247. const types = [];
  1248. if (payload.includeStation) types.push("station");
  1249. if (payload.includeUser) types.push("user");
  1250. if (payload.includeGenre) types.push("genre");
  1251. if (payload.includeArtist) types.push("artist");
  1252. if (types.length === 0 && !payload.includeOwn) return next("No types have been included.");
  1253. const privacies = ["public"];
  1254. if (payload.includePrivate) privacies.push("private");
  1255. const includeObject = payload.includeSongs ? null : { songs: false };
  1256. const filterArray = [
  1257. {
  1258. displayName: new RegExp(`${payload.query}`, "i"),
  1259. privacy: { $in: privacies },
  1260. type: { $in: types }
  1261. }
  1262. ];
  1263. if (payload.includeOwn && payload.userId)
  1264. filterArray.push({
  1265. displayName: new RegExp(`${payload.query}`, "i"),
  1266. type: "user",
  1267. createdBy: payload.userId
  1268. });
  1269. return next(null, filterArray, includeObject);
  1270. },
  1271. (filterArray, includeObject, next) => {
  1272. const page = payload.page ? payload.page : 1;
  1273. const pageSize = 15;
  1274. const skipAmount = pageSize * (page - 1);
  1275. PlaylistsModule.playlistModel.find({ $or: filterArray }).count((err, count) => {
  1276. if (err) next(err);
  1277. else {
  1278. PlaylistsModule.playlistModel
  1279. .find({ $or: filterArray }, includeObject)
  1280. .skip(skipAmount)
  1281. .limit(pageSize)
  1282. .exec((err, playlists) => {
  1283. if (err) next(err);
  1284. else {
  1285. next(null, {
  1286. playlists,
  1287. page,
  1288. pageSize,
  1289. skipAmount,
  1290. count
  1291. });
  1292. }
  1293. });
  1294. }
  1295. });
  1296. },
  1297. (data, next) => {
  1298. if (data.playlists.length > 0) next(null, data);
  1299. else next("No playlists found");
  1300. }
  1301. ],
  1302. (err, data) => {
  1303. if (err && err !== true) return reject(new Error(err));
  1304. return resolve(data);
  1305. }
  1306. );
  1307. });
  1308. }
  1309. /**
  1310. * Clears and refills a station playlist
  1311. *
  1312. * @param {object} payload - object that contains the payload
  1313. * @param {string} payload.playlistId - the id of the playlist we are trying to clear and refill
  1314. * @returns {Promise} - returns promise (reject, resolve)
  1315. */
  1316. CLEAR_AND_REFILL_STATION_PLAYLIST(payload) {
  1317. return new Promise((resolve, reject) => {
  1318. const { playlistId } = payload;
  1319. async.waterfall(
  1320. [
  1321. next => {
  1322. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  1323. .then(playlist => {
  1324. next(null, playlist);
  1325. })
  1326. .catch(err => {
  1327. next(err);
  1328. });
  1329. },
  1330. (playlist, next) => {
  1331. if (playlist.type !== "station") next("This playlist is not a station playlist.");
  1332. else next(null, playlist.createdFor);
  1333. },
  1334. (stationId, next) => {
  1335. PlaylistsModule.runJob("AUTOFILL_STATION_PLAYLIST", { stationId }, this)
  1336. .then(() => {
  1337. next();
  1338. })
  1339. .catch(err => {
  1340. next(err);
  1341. });
  1342. }
  1343. ],
  1344. err => {
  1345. if (err && err !== true) return reject(new Error(err));
  1346. return resolve();
  1347. }
  1348. );
  1349. });
  1350. }
  1351. /**
  1352. * Clears and refills a genre playlist
  1353. *
  1354. * @param {object} payload - object that contains the payload
  1355. * @param {string} payload.playlistId - the id of the playlist we are trying to clear and refill
  1356. * @returns {Promise} - returns promise (reject, resolve)
  1357. */
  1358. CLEAR_AND_REFILL_GENRE_PLAYLIST(payload) {
  1359. return new Promise((resolve, reject) => {
  1360. const { playlistId } = payload;
  1361. async.waterfall(
  1362. [
  1363. next => {
  1364. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  1365. .then(playlist => {
  1366. next(null, playlist);
  1367. })
  1368. .catch(err => {
  1369. next(err);
  1370. });
  1371. },
  1372. (playlist, next) => {
  1373. if (playlist.type !== "genre") next("This playlist is not a genre playlist.");
  1374. else next(null, playlist.createdFor);
  1375. },
  1376. (genre, next) => {
  1377. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre, createPlaylist: true }, this)
  1378. .then(() => {
  1379. next();
  1380. })
  1381. .catch(err => {
  1382. next(err);
  1383. });
  1384. }
  1385. ],
  1386. err => {
  1387. if (err && err !== true) return reject(new Error(err));
  1388. return resolve();
  1389. }
  1390. );
  1391. });
  1392. }
  1393. /**
  1394. * Clears and refills a artist playlist
  1395. *
  1396. * @param {object} payload - object that contains the payload
  1397. * @param {string} payload.playlistId - the id of the playlist we are trying to clear and refill
  1398. * @returns {Promise} - returns promise (reject, resolve)
  1399. */
  1400. CLEAR_AND_REFILL_ARTIST_PLAYLIST(payload) {
  1401. return new Promise((resolve, reject) => {
  1402. const { playlistId } = payload;
  1403. async.waterfall(
  1404. [
  1405. next => {
  1406. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  1407. .then(playlist => {
  1408. next(null, playlist);
  1409. })
  1410. .catch(err => {
  1411. next(err);
  1412. });
  1413. },
  1414. (playlist, next) => {
  1415. if (playlist.type !== "artist") next("This playlist is not a artist playlist.");
  1416. else next(null, playlist.createdFor);
  1417. },
  1418. (artist, next) => {
  1419. PlaylistsModule.runJob("AUTOFILL_ARTIST_PLAYLIST", { artist }, this)
  1420. .then(() => {
  1421. next();
  1422. })
  1423. .catch(err => {
  1424. next(err);
  1425. });
  1426. }
  1427. ],
  1428. err => {
  1429. if (err && err !== true) return reject(new Error(err));
  1430. return resolve();
  1431. }
  1432. );
  1433. });
  1434. }
  1435. }
  1436. export default new _PlaylistsModule();