playlists.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113
  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. * Creates a playlist that is not generated or editable by a user e.g. liked songs playlist
  99. *
  100. * @param {object} payload - object that contains the payload
  101. * @param {string} payload.userId - the id of the user to create the playlist for
  102. * @param {string} payload.displayName - the display name of the playlist
  103. * @returns {Promise} - returns promise (reject, resolve)
  104. */
  105. CREATE_READ_ONLY_PLAYLIST(payload) {
  106. return new Promise((resolve, reject) => {
  107. PlaylistsModule.playlistModel.create(
  108. {
  109. isUserModifiable: false,
  110. displayName: payload.displayName,
  111. songs: [],
  112. createdBy: payload.userId,
  113. createdAt: Date.now(),
  114. createdFor: null,
  115. type: payload.type
  116. },
  117. (err, playlist) => {
  118. if (err) return reject(new Error(err));
  119. return resolve(playlist._id);
  120. }
  121. );
  122. });
  123. }
  124. /**
  125. * Creates a playlist that contains all songs of a specific genre
  126. *
  127. * @param {object} payload - object that contains the payload
  128. * @param {string} payload.genre - the genre
  129. * @returns {Promise} - returns promise (reject, resolve)
  130. */
  131. CREATE_GENRE_PLAYLIST(payload) {
  132. return new Promise((resolve, reject) => {
  133. PlaylistsModule.runJob("GET_GENRE_PLAYLIST", { genre: payload.genre.toLowerCase() }, this)
  134. .then(() => {
  135. reject(new Error("Playlist already exists"));
  136. })
  137. .catch(err => {
  138. if (err.message === "Playlist not found") {
  139. PlaylistsModule.playlistModel.create(
  140. {
  141. isUserModifiable: false,
  142. displayName: `Genre - ${payload.genre}`,
  143. songs: [],
  144. createdBy: "Musare",
  145. createdFor: `${payload.genre.toLowerCase()}`,
  146. createdAt: Date.now(),
  147. type: "genre"
  148. },
  149. (err, playlist) => {
  150. if (err) return reject(new Error(err));
  151. return resolve(playlist._id);
  152. }
  153. );
  154. } else reject(new Error(err));
  155. });
  156. });
  157. }
  158. /**
  159. * Gets all genre playlists
  160. *
  161. * @param {object} payload - object that contains the payload
  162. * @param {string} payload.includeSongs - include the songs
  163. * @returns {Promise} - returns promise (reject, resolve)
  164. */
  165. GET_ALL_GENRE_PLAYLISTS(payload) {
  166. return new Promise((resolve, reject) => {
  167. const includeObject = payload.includeSongs ? null : { songs: false };
  168. PlaylistsModule.playlistModel.find({ type: "genre" }, includeObject, (err, playlists) => {
  169. if (err) reject(new Error(err));
  170. else resolve({ playlists });
  171. });
  172. });
  173. }
  174. /**
  175. * Gets all station playlists
  176. *
  177. * @param {object} payload - object that contains the payload
  178. * @param {string} payload.includeSongs - include the songs
  179. * @returns {Promise} - returns promise (reject, resolve)
  180. */
  181. GET_ALL_STATION_PLAYLISTS(payload) {
  182. return new Promise((resolve, reject) => {
  183. const includeObject = payload.includeSongs ? null : { songs: false };
  184. PlaylistsModule.playlistModel.find({ type: "station" }, includeObject, (err, playlists) => {
  185. if (err) reject(new Error(err));
  186. else resolve({ playlists });
  187. });
  188. });
  189. }
  190. /**
  191. * Gets a genre playlist
  192. *
  193. * @param {object} payload - object that contains the payload
  194. * @param {string} payload.genre - the genre
  195. * @param {string} payload.includeSongs - include the songs
  196. * @returns {Promise} - returns promise (reject, resolve)
  197. */
  198. GET_GENRE_PLAYLIST(payload) {
  199. return new Promise((resolve, reject) => {
  200. const includeObject = payload.includeSongs ? null : { songs: false };
  201. PlaylistsModule.playlistModel.findOne(
  202. { type: "genre", createdFor: payload.genre },
  203. includeObject,
  204. (err, playlist) => {
  205. if (err) reject(new Error(err));
  206. else if (!playlist) reject(new Error("Playlist not found"));
  207. else resolve({ playlist });
  208. }
  209. );
  210. });
  211. }
  212. /**
  213. * Gets all missing genre playlists
  214. *
  215. * @returns {Promise} - returns promise (reject, resolve)
  216. */
  217. GET_MISSING_GENRE_PLAYLISTS() {
  218. return new Promise((resolve, reject) => {
  219. SongsModule.runJob("GET_ALL_GENRES", {}, this)
  220. .then(response => {
  221. const { genres } = response;
  222. const missingGenres = [];
  223. async.eachLimit(
  224. genres,
  225. 1,
  226. (genre, next) => {
  227. PlaylistsModule.runJob(
  228. "GET_GENRE_PLAYLIST",
  229. { genre: genre.toLowerCase(), includeSongs: false },
  230. this
  231. )
  232. .then(() => {
  233. next();
  234. })
  235. .catch(err => {
  236. if (err.message === "Playlist not found") {
  237. missingGenres.push(genre);
  238. next();
  239. } else next(err);
  240. });
  241. },
  242. err => {
  243. if (err) reject(err);
  244. else resolve({ genres: missingGenres });
  245. }
  246. );
  247. })
  248. .catch(err => {
  249. reject(err);
  250. });
  251. });
  252. }
  253. /**
  254. * Creates all missing genre playlists
  255. *
  256. * @returns {Promise} - returns promise (reject, resolve)
  257. */
  258. CREATE_MISSING_GENRE_PLAYLISTS() {
  259. return new Promise((resolve, reject) => {
  260. PlaylistsModule.runJob("GET_MISSING_GENRE_PLAYLISTS", {}, this)
  261. .then(response => {
  262. const { genres } = response;
  263. async.eachLimit(
  264. genres,
  265. 1,
  266. (genre, next) => {
  267. PlaylistsModule.runJob("CREATE_GENRE_PLAYLIST", { genre }, this)
  268. .then(() => {
  269. next();
  270. })
  271. .catch(err => {
  272. next(err);
  273. });
  274. },
  275. err => {
  276. if (err) reject(err);
  277. else resolve();
  278. }
  279. );
  280. })
  281. .catch(err => {
  282. reject(err);
  283. });
  284. });
  285. }
  286. /**
  287. * Gets a station playlist
  288. *
  289. * @param {object} payload - object that contains the payload
  290. * @param {string} payload.staationId - the station id
  291. * @param {string} payload.includeSongs - include the songs
  292. * @returns {Promise} - returns promise (reject, resolve)
  293. */
  294. GET_STATION_PLAYLIST(payload) {
  295. return new Promise((resolve, reject) => {
  296. const includeObject = payload.includeSongs ? null : { songs: false };
  297. PlaylistsModule.playlistModel.findOne(
  298. { type: "station", createdFor: payload.stationId },
  299. includeObject,
  300. (err, playlist) => {
  301. if (err) reject(new Error(err));
  302. else if (!playlist) reject(new Error("Playlist not found"));
  303. else resolve({ playlist });
  304. }
  305. );
  306. });
  307. }
  308. /**
  309. * Adds a song to a playlist
  310. *
  311. * @param {object} payload - object that contains the payload
  312. * @param {string} payload.playlistId - the playlist id
  313. * @param {string} payload.song - the song
  314. * @returns {Promise} - returns promise (reject, resolve)
  315. */
  316. ADD_SONG_TO_PLAYLIST(payload) {
  317. return new Promise((resolve, reject) => {
  318. const { _id, youtubeId, title, artists, thumbnail, duration, status } = payload.song;
  319. const trimmedSong = {
  320. _id,
  321. youtubeId,
  322. title,
  323. artists,
  324. thumbnail,
  325. duration,
  326. status
  327. };
  328. PlaylistsModule.playlistModel.updateOne(
  329. { _id: payload.playlistId },
  330. { $push: { songs: trimmedSong } },
  331. { runValidators: true },
  332. err => {
  333. if (err) reject(new Error(err));
  334. else {
  335. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId: payload.playlistId }, this)
  336. .then(() => resolve())
  337. .catch(err => {
  338. reject(new Error(err));
  339. });
  340. }
  341. }
  342. );
  343. });
  344. }
  345. /**
  346. * Deletes a song from a playlist based on the youtube id
  347. *
  348. * @param {object} payload - object that contains the payload
  349. * @param {string} payload.playlistId - the playlist id
  350. * @param {string} payload.youtubeId - the youtube id
  351. * @returns {Promise} - returns promise (reject, resolve)
  352. */
  353. DELETE_SONG_FROM_PLAYLIST_BY_YOUTUBE_ID(payload) {
  354. return new Promise((resolve, reject) => {
  355. PlaylistsModule.playlistModel.updateOne(
  356. { _id: payload.playlistId },
  357. { $pull: { songs: { youtubeId: payload.youtubeId } } },
  358. err => {
  359. if (err) reject(new Error(err));
  360. else {
  361. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId: payload.playlistId }, this)
  362. .then(() => resolve())
  363. .catch(err => {
  364. reject(new Error(err));
  365. });
  366. }
  367. }
  368. );
  369. });
  370. }
  371. /**
  372. * Fills a genre playlist with songs
  373. *
  374. * @param {object} payload - object that contains the payload
  375. * @param {string} payload.genre - the genre
  376. * @returns {Promise} - returns promise (reject, resolve)
  377. */
  378. AUTOFILL_GENRE_PLAYLIST(payload) {
  379. return new Promise((resolve, reject) => {
  380. async.waterfall(
  381. [
  382. next => {
  383. PlaylistsModule.runJob(
  384. "GET_GENRE_PLAYLIST",
  385. { genre: payload.genre.toLowerCase(), includeSongs: true },
  386. this
  387. )
  388. .then(response => {
  389. next(null, response.playlist._id);
  390. })
  391. .catch(err => {
  392. if (err.message === "Playlist not found") {
  393. PlaylistsModule.runJob("CREATE_GENRE_PLAYLIST", { genre: payload.genre }, this)
  394. .then(playlistId => {
  395. next(null, playlistId);
  396. })
  397. .catch(err => {
  398. next(err);
  399. });
  400. } else next(err);
  401. });
  402. },
  403. (playlistId, next) => {
  404. SongsModule.runJob("GET_ALL_SONGS_WITH_GENRE", { genre: payload.genre }, this)
  405. .then(response => {
  406. next(null, playlistId, response.songs);
  407. })
  408. .catch(err => {
  409. console.log(err);
  410. next(err);
  411. });
  412. },
  413. (playlistId, _songs, next) => {
  414. const songs = _songs.map(song => {
  415. const { _id, youtubeId, title, artists, thumbnail, duration, status } = song;
  416. return {
  417. _id,
  418. youtubeId,
  419. title,
  420. artists,
  421. thumbnail,
  422. duration,
  423. status
  424. };
  425. });
  426. PlaylistsModule.playlistModel.updateOne({ _id: playlistId }, { $set: { songs } }, err => {
  427. next(err, playlistId);
  428. });
  429. },
  430. (playlistId, next) => {
  431. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  432. .then(() => {
  433. next(null, playlistId);
  434. })
  435. .catch(next);
  436. },
  437. (playlistId, next) => {
  438. StationsModule.runJob("GET_STATIONS_THAT_INCLUDE_OR_EXCLUDE_PLAYLIST", { playlistId }, this)
  439. .then(response => {
  440. async.eachLimit(
  441. response.stationIds,
  442. 1,
  443. (stationId, next) => {
  444. PlaylistsModule.runJob("AUTOFILL_STATION_PLAYLIST", { stationId }, this)
  445. .then(() => {
  446. next();
  447. })
  448. .catch(err => {
  449. next(err);
  450. });
  451. },
  452. err => {
  453. if (err) next(err);
  454. else next();
  455. }
  456. );
  457. })
  458. .catch(err => {
  459. next(err);
  460. });
  461. }
  462. ],
  463. err => {
  464. if (err && err !== true) return reject(new Error(err));
  465. return resolve({});
  466. }
  467. );
  468. });
  469. }
  470. /**
  471. * Gets orphaned genre playlists
  472. *
  473. * @returns {Promise} - returns promise (reject, resolve)
  474. */
  475. GET_ORPHANED_GENRE_PLAYLISTS() {
  476. return new Promise((resolve, reject) => {
  477. PlaylistsModule.playlistModel.find({ type: "genre" }, { songs: false }, (err, playlists) => {
  478. if (err) reject(new Error(err));
  479. else {
  480. const orphanedPlaylists = [];
  481. async.eachLimit(
  482. playlists,
  483. 1,
  484. (playlist, next) => {
  485. SongsModule.runJob("GET_ALL_SONGS_WITH_GENRE", { genre: playlist.createdFor }, this)
  486. .then(response => {
  487. if (response.songs.length === 0) {
  488. StationsModule.runJob(
  489. "GET_STATIONS_THAT_INCLUDE_OR_EXCLUDE_PLAYLIST",
  490. { playlistId: playlist._id },
  491. this
  492. )
  493. .then(response => {
  494. if (response.stationIds.length === 0) orphanedPlaylists.push(playlist);
  495. next();
  496. })
  497. .catch(next);
  498. } else next();
  499. })
  500. .catch(next);
  501. },
  502. err => {
  503. if (err) reject(new Error(err));
  504. else resolve({ playlists: orphanedPlaylists });
  505. }
  506. );
  507. }
  508. });
  509. });
  510. }
  511. /**
  512. * Deletes all orphaned genre playlists
  513. *
  514. * @returns {Promise} - returns promise (reject, resolve)
  515. */
  516. DELETE_ORPHANED_GENRE_PLAYLISTS() {
  517. return new Promise((resolve, reject) => {
  518. PlaylistsModule.runJob("GET_ORPHANED_GENRE_PLAYLISTS", {}, this)
  519. .then(response => {
  520. async.eachLimit(
  521. response.playlists,
  522. 1,
  523. (playlist, next) => {
  524. PlaylistsModule.runJob("DELETE_PLAYLIST", { playlistId: playlist._id }, this)
  525. .then(() => {
  526. this.log("INFO", "Deleting orphaned genre playlist");
  527. next();
  528. })
  529. .catch(err => {
  530. next(err);
  531. });
  532. },
  533. err => {
  534. if (err) reject(new Error(err));
  535. else resolve({});
  536. }
  537. );
  538. })
  539. .catch(err => {
  540. reject(new Error(err));
  541. });
  542. });
  543. }
  544. /**
  545. * Gets a orphaned station playlists
  546. *
  547. * @returns {Promise} - returns promise (reject, resolve)
  548. */
  549. GET_ORPHANED_STATION_PLAYLISTS() {
  550. return new Promise((resolve, reject) => {
  551. PlaylistsModule.playlistModel.find({ type: "station" }, { songs: false }, (err, playlists) => {
  552. if (err) reject(new Error(err));
  553. else {
  554. const orphanedPlaylists = [];
  555. async.eachLimit(
  556. playlists,
  557. 1,
  558. (playlist, next) => {
  559. StationsModule.runJob("GET_STATION", { stationId: playlist.createdFor }, this)
  560. .then(station => {
  561. if (station.playlist !== playlist._id.toString()) {
  562. orphanedPlaylists.push(playlist);
  563. }
  564. next();
  565. })
  566. .catch(err => {
  567. if (err.message === "Station not found") {
  568. orphanedPlaylists.push(playlist);
  569. next();
  570. } else next(err);
  571. });
  572. },
  573. err => {
  574. if (err) reject(new Error(err));
  575. else resolve({ playlists: orphanedPlaylists });
  576. }
  577. );
  578. }
  579. });
  580. });
  581. }
  582. /**
  583. * Deletes all orphaned station playlists
  584. *
  585. * @returns {Promise} - returns promise (reject, resolve)
  586. */
  587. DELETE_ORPHANED_STATION_PLAYLISTS() {
  588. return new Promise((resolve, reject) => {
  589. PlaylistsModule.runJob("GET_ORPHANED_STATION_PLAYLISTS", {}, this)
  590. .then(response => {
  591. async.eachLimit(
  592. response.playlists,
  593. 1,
  594. (playlist, next) => {
  595. PlaylistsModule.runJob("DELETE_PLAYLIST", { playlistId: playlist._id }, this)
  596. .then(() => {
  597. this.log("INFO", "Deleting orphaned station playlist");
  598. next();
  599. })
  600. .catch(err => {
  601. next(err);
  602. });
  603. },
  604. err => {
  605. if (err) reject(new Error(err));
  606. else resolve({});
  607. }
  608. );
  609. })
  610. .catch(err => {
  611. reject(new Error(err));
  612. });
  613. });
  614. }
  615. /**
  616. * Fills a station playlist with songs
  617. *
  618. * @param {object} payload - object that contains the payload
  619. * @param {string} payload.stationId - the station id
  620. * @returns {Promise} - returns promise (reject, resolve)
  621. */
  622. AUTOFILL_STATION_PLAYLIST(payload) {
  623. return new Promise((resolve, reject) => {
  624. let originalPlaylist = null;
  625. async.waterfall(
  626. [
  627. next => {
  628. if (!payload.stationId) next("Please specify a station id");
  629. else next();
  630. },
  631. next => {
  632. StationsModule.runJob("GET_STATION", { stationId: payload.stationId }, this)
  633. .then(station => {
  634. next(null, station);
  635. })
  636. .catch(next);
  637. },
  638. (station, next) => {
  639. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId: station.playlist }, this)
  640. .then(playlist => {
  641. originalPlaylist = playlist;
  642. next(null, station);
  643. })
  644. .catch(err => {
  645. next(err);
  646. });
  647. },
  648. (station, next) => {
  649. const includedPlaylists = [];
  650. async.eachLimit(
  651. station.includedPlaylists,
  652. 1,
  653. (playlistId, next) => {
  654. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  655. .then(playlist => {
  656. includedPlaylists.push(playlist);
  657. next();
  658. })
  659. .catch(next);
  660. },
  661. err => {
  662. next(err, station, includedPlaylists);
  663. }
  664. );
  665. },
  666. (station, includedPlaylists, next) => {
  667. const excludedPlaylists = [];
  668. async.eachLimit(
  669. station.excludedPlaylists,
  670. 1,
  671. (playlistId, next) => {
  672. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  673. .then(playlist => {
  674. excludedPlaylists.push(playlist);
  675. next();
  676. })
  677. .catch(next);
  678. },
  679. err => {
  680. next(err, station, includedPlaylists, excludedPlaylists);
  681. }
  682. );
  683. },
  684. (station, includedPlaylists, excludedPlaylists, next) => {
  685. const excludedSongs = excludedPlaylists
  686. .flatMap(excludedPlaylist => excludedPlaylist.songs)
  687. .reduce(
  688. (items, item) =>
  689. items.find(x => x.youtubeId === item.youtubeId) ? [...items] : [...items, item],
  690. []
  691. );
  692. const includedSongs = includedPlaylists
  693. .flatMap(includedPlaylist => includedPlaylist.songs)
  694. .reduce(
  695. (songs, song) =>
  696. songs.find(x => x.youtubeId === song.youtubeId) ? [...songs] : [...songs, song],
  697. []
  698. )
  699. .filter(song => !excludedSongs.find(x => x.youtubeId === song.youtubeId));
  700. next(null, station, includedSongs);
  701. },
  702. (station, includedSongs, next) => {
  703. PlaylistsModule.playlistModel.updateOne(
  704. { _id: station.playlist },
  705. { $set: { songs: includedSongs } },
  706. err => {
  707. next(err, includedSongs);
  708. }
  709. );
  710. },
  711. (includedSongs, next) => {
  712. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId: originalPlaylist._id }, this)
  713. .then(() => {
  714. next(null, includedSongs);
  715. })
  716. .catch(next);
  717. },
  718. (includedSongs, next) => {
  719. if (originalPlaylist.songs.length === 0 && includedSongs.length > 0)
  720. StationsModule.runJob("SKIP_STATION", { stationId: payload.stationId, natural: false });
  721. next();
  722. }
  723. ],
  724. err => {
  725. if (err && err !== true) return reject(new Error(err));
  726. return resolve({});
  727. }
  728. );
  729. });
  730. }
  731. /**
  732. * Gets a playlist by id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
  733. *
  734. * @param {object} payload - object that contains the payload
  735. * @param {string} payload.playlistId - the id of the playlist we are trying to get
  736. * @returns {Promise} - returns promise (reject, resolve)
  737. */
  738. GET_PLAYLIST(payload) {
  739. return new Promise((resolve, reject) =>
  740. async.waterfall(
  741. [
  742. next => {
  743. CacheModule.runJob(
  744. "HGET",
  745. {
  746. table: "playlists",
  747. key: payload.playlistId
  748. },
  749. this
  750. )
  751. .then(playlist => next(null, playlist))
  752. .catch(next);
  753. },
  754. (playlist, next) => {
  755. if (playlist)
  756. PlaylistsModule.playlistModel.exists({ _id: payload.playlistId }, (err, exists) => {
  757. if (err) next(err);
  758. else if (exists) next(null, playlist);
  759. else {
  760. CacheModule.runJob(
  761. "HDEL",
  762. {
  763. table: "playlists",
  764. key: payload.playlistId
  765. },
  766. this
  767. )
  768. .then(() => next())
  769. .catch(next);
  770. }
  771. });
  772. else PlaylistsModule.playlistModel.findOne({ _id: payload.playlistId }, next);
  773. },
  774. (playlist, next) => {
  775. if (playlist) {
  776. CacheModule.runJob(
  777. "HSET",
  778. {
  779. table: "playlists",
  780. key: payload.playlistId,
  781. value: playlist
  782. },
  783. this
  784. )
  785. .then(playlist => {
  786. next(null, playlist);
  787. })
  788. .catch(next);
  789. } else next("Playlist not found");
  790. }
  791. ],
  792. (err, playlist) => {
  793. if (err && err !== true) return reject(new Error(err));
  794. return resolve(playlist);
  795. }
  796. )
  797. );
  798. }
  799. /**
  800. * Gets a playlist from id from Mongo and updates the cache with it
  801. *
  802. * @param {object} payload - object that contains the payload
  803. * @param {string} payload.playlistId - the id of the playlist we are trying to update
  804. * @returns {Promise} - returns promise (reject, resolve)
  805. */
  806. UPDATE_PLAYLIST(payload) {
  807. return new Promise((resolve, reject) =>
  808. async.waterfall(
  809. [
  810. next => {
  811. PlaylistsModule.playlistModel.findOne({ _id: payload.playlistId }, next);
  812. },
  813. (playlist, next) => {
  814. if (!playlist) {
  815. CacheModule.runJob("HDEL", {
  816. table: "playlists",
  817. key: payload.playlistId
  818. });
  819. return next("Playlist not found");
  820. }
  821. return CacheModule.runJob(
  822. "HSET",
  823. {
  824. table: "playlists",
  825. key: payload.playlistId,
  826. value: playlist
  827. },
  828. this
  829. )
  830. .then(playlist => {
  831. next(null, playlist);
  832. })
  833. .catch(next);
  834. }
  835. ],
  836. (err, playlist) => {
  837. if (err && err !== true) return reject(new Error(err));
  838. return resolve(playlist);
  839. }
  840. )
  841. );
  842. }
  843. /**
  844. * Deletes playlist from id from Mongo and cache
  845. *
  846. * @param {object} payload - object that contains the payload
  847. * @param {string} payload.playlistId - the id of the playlist we are trying to delete
  848. * @returns {Promise} - returns promise (reject, resolve)
  849. */
  850. DELETE_PLAYLIST(payload) {
  851. return new Promise((resolve, reject) =>
  852. async.waterfall(
  853. [
  854. next => {
  855. PlaylistsModule.playlistModel.deleteOne({ _id: payload.playlistId }, next);
  856. },
  857. (res, next) => {
  858. CacheModule.runJob(
  859. "HDEL",
  860. {
  861. table: "playlists",
  862. key: payload.playlistId
  863. },
  864. this
  865. )
  866. .then(() => next())
  867. .catch(next);
  868. }
  869. ],
  870. err => {
  871. if (err && err !== true) return reject(new Error(err));
  872. return resolve();
  873. }
  874. )
  875. );
  876. }
  877. /**
  878. * Searches through playlists
  879. *
  880. * @param {object} payload - object that contains the payload
  881. * @param {string} payload.query - the query
  882. * @param {string} payload.includePrivate - include private playlists
  883. * @param {string} payload.includeStation - include station playlists
  884. * @param {string} payload.includeUser - include user playlists
  885. * @param {string} payload.includeGenre - include genre playlists
  886. * @param {string} payload.includeOwn - include own user playlists
  887. * @param {string} payload.userId - the user id of the person requesting
  888. * @param {string} payload.includeSongs - include songs
  889. * @param {string} payload.page - page (default 1)
  890. * @returns {Promise} - returns promise (reject, resolve)
  891. */
  892. SEARCH(payload) {
  893. return new Promise((resolve, reject) =>
  894. async.waterfall(
  895. [
  896. next => {
  897. const types = [];
  898. if (payload.includeStation) types.push("station");
  899. if (payload.includeUser) types.push("user");
  900. if (payload.includeGenre) types.push("genre");
  901. if (types.length === 0 && !payload.includeOwn) return next("No types have been included.");
  902. const privacies = ["public"];
  903. if (payload.includePrivate) privacies.push("private");
  904. const includeObject = payload.includeSongs ? null : { songs: false };
  905. const filterArray = [
  906. {
  907. displayName: new RegExp(`${payload.query}`, "i"),
  908. privacy: { $in: privacies },
  909. type: { $in: types }
  910. }
  911. ];
  912. if (payload.includeOwn && payload.userId)
  913. filterArray.push({
  914. displayName: new RegExp(`${payload.query}`, "i"),
  915. type: "user",
  916. createdBy: payload.userId
  917. });
  918. return next(null, filterArray, includeObject);
  919. },
  920. (filterArray, includeObject, next) => {
  921. const page = payload.page ? payload.page : 1;
  922. const pageSize = 15;
  923. const skipAmount = pageSize * (page - 1);
  924. PlaylistsModule.playlistModel.find({ $or: filterArray }).count((err, count) => {
  925. if (err) next(err);
  926. else {
  927. PlaylistsModule.playlistModel
  928. .find({ $or: filterArray }, includeObject)
  929. .skip(skipAmount)
  930. .limit(pageSize)
  931. .exec((err, playlists) => {
  932. if (err) next(err);
  933. else {
  934. next(null, {
  935. playlists,
  936. page,
  937. pageSize,
  938. skipAmount,
  939. count
  940. });
  941. }
  942. });
  943. }
  944. });
  945. },
  946. (data, next) => {
  947. if (data.playlists.length > 0) next(null, data);
  948. else next("No playlists found");
  949. }
  950. ],
  951. (err, data) => {
  952. if (err && err !== true) return reject(new Error(err));
  953. return resolve(data);
  954. }
  955. )
  956. );
  957. }
  958. /**
  959. * Clears and refills a station playlist
  960. *
  961. * @param {object} payload - object that contains the payload
  962. * @param {string} payload.playlistId - the id of the playlist we are trying to clear and refill
  963. * @returns {Promise} - returns promise (reject, resolve)
  964. */
  965. CLEAR_AND_REFILL_STATION_PLAYLIST(payload) {
  966. return new Promise((resolve, reject) => {
  967. const { playlistId } = payload;
  968. async.waterfall(
  969. [
  970. next => {
  971. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  972. .then(playlist => {
  973. next(null, playlist);
  974. })
  975. .catch(err => {
  976. next(err);
  977. });
  978. },
  979. (playlist, next) => {
  980. if (playlist.type !== "station") next("This playlist is not a station playlist.");
  981. else next(null, playlist.createdFor);
  982. },
  983. (stationId, next) => {
  984. PlaylistsModule.runJob("AUTOFILL_STATION_PLAYLIST", { stationId }, this)
  985. .then(() => {
  986. next();
  987. })
  988. .catch(err => {
  989. next(err);
  990. });
  991. }
  992. ],
  993. err => {
  994. if (err && err !== true) return reject(new Error(err));
  995. return resolve();
  996. }
  997. );
  998. });
  999. }
  1000. /**
  1001. * Clears and refills a genre playlist
  1002. *
  1003. * @param {object} payload - object that contains the payload
  1004. * @param {string} payload.playlistId - the id of the playlist we are trying to clear and refill
  1005. * @returns {Promise} - returns promise (reject, resolve)
  1006. */
  1007. CLEAR_AND_REFILL_GENRE_PLAYLIST(payload) {
  1008. return new Promise((resolve, reject) => {
  1009. const { playlistId } = payload;
  1010. async.waterfall(
  1011. [
  1012. next => {
  1013. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  1014. .then(playlist => {
  1015. next(null, playlist);
  1016. })
  1017. .catch(err => {
  1018. next(err);
  1019. });
  1020. },
  1021. (playlist, next) => {
  1022. if (playlist.type !== "genre") next("This playlist is not a genre playlist.");
  1023. else next(null, playlist.createdFor);
  1024. },
  1025. (genre, next) => {
  1026. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre }, this)
  1027. .then(() => {
  1028. next();
  1029. })
  1030. .catch(err => {
  1031. next(err);
  1032. });
  1033. }
  1034. ],
  1035. err => {
  1036. if (err && err !== true) return reject(new Error(err));
  1037. return resolve();
  1038. }
  1039. );
  1040. });
  1041. }
  1042. }
  1043. export default new _PlaylistsModule();