playlists.js 32 KB

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