playlists.js 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147
  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 a playlist from id from Mongo and updates the cache with it
  821. *
  822. * @param {object} payload - object that contains the payload
  823. * @param {string} payload.playlistId - the id of the playlist we are trying to update
  824. * @returns {Promise} - returns promise (reject, resolve)
  825. */
  826. UPDATE_PLAYLIST(payload) {
  827. return new Promise((resolve, reject) =>
  828. async.waterfall(
  829. [
  830. next => {
  831. PlaylistsModule.playlistModel.findOne({ _id: payload.playlistId }, next);
  832. },
  833. (playlist, next) => {
  834. if (!playlist) {
  835. CacheModule.runJob("HDEL", {
  836. table: "playlists",
  837. key: payload.playlistId
  838. });
  839. return next("Playlist not found");
  840. }
  841. return CacheModule.runJob(
  842. "HSET",
  843. {
  844. table: "playlists",
  845. key: payload.playlistId,
  846. value: playlist
  847. },
  848. this
  849. )
  850. .then(playlist => {
  851. next(null, playlist);
  852. })
  853. .catch(next);
  854. }
  855. ],
  856. (err, playlist) => {
  857. if (err && err !== true) return reject(new Error(err));
  858. return resolve(playlist);
  859. }
  860. )
  861. );
  862. }
  863. /**
  864. * Deletes playlist from id from Mongo and cache
  865. *
  866. * @param {object} payload - object that contains the payload
  867. * @param {string} payload.playlistId - the id of the playlist we are trying to delete
  868. * @returns {Promise} - returns promise (reject, resolve)
  869. */
  870. DELETE_PLAYLIST(payload) {
  871. return new Promise((resolve, reject) =>
  872. async.waterfall(
  873. [
  874. next => {
  875. PlaylistsModule.playlistModel.deleteOne({ _id: payload.playlistId }, next);
  876. },
  877. (res, next) => {
  878. CacheModule.runJob(
  879. "HDEL",
  880. {
  881. table: "playlists",
  882. key: payload.playlistId
  883. },
  884. this
  885. )
  886. .then(() => next())
  887. .catch(next);
  888. },
  889. next => {
  890. StationsModule.runJob(
  891. "REMOVE_INCLUDED_OR_EXCLUDED_PLAYLIST_FROM_STATIONS",
  892. { playlistId: payload.playlistId },
  893. this
  894. )
  895. .then(() => {
  896. next();
  897. })
  898. .catch(err => next(err));
  899. }
  900. ],
  901. err => {
  902. if (err && err !== true) return reject(new Error(err));
  903. return resolve();
  904. }
  905. )
  906. );
  907. }
  908. /**
  909. * Searches through playlists
  910. *
  911. * @param {object} payload - object that contains the payload
  912. * @param {string} payload.query - the query
  913. * @param {string} payload.includePrivate - include private playlists
  914. * @param {string} payload.includeStation - include station playlists
  915. * @param {string} payload.includeUser - include user playlists
  916. * @param {string} payload.includeGenre - include genre playlists
  917. * @param {string} payload.includeOwn - include own user playlists
  918. * @param {string} payload.userId - the user id of the person requesting
  919. * @param {string} payload.includeSongs - include songs
  920. * @param {string} payload.page - page (default 1)
  921. * @returns {Promise} - returns promise (reject, resolve)
  922. */
  923. SEARCH(payload) {
  924. return new Promise((resolve, reject) =>
  925. async.waterfall(
  926. [
  927. next => {
  928. const types = [];
  929. if (payload.includeStation) types.push("station");
  930. if (payload.includeUser) types.push("user");
  931. if (payload.includeGenre) types.push("genre");
  932. if (types.length === 0 && !payload.includeOwn) return next("No types have been included.");
  933. const privacies = ["public"];
  934. if (payload.includePrivate) privacies.push("private");
  935. const includeObject = payload.includeSongs ? null : { songs: false };
  936. const filterArray = [
  937. {
  938. displayName: new RegExp(`${payload.query}`, "i"),
  939. privacy: { $in: privacies },
  940. type: { $in: types }
  941. }
  942. ];
  943. if (payload.includeOwn && payload.userId)
  944. filterArray.push({
  945. displayName: new RegExp(`${payload.query}`, "i"),
  946. type: "user",
  947. createdBy: payload.userId
  948. });
  949. return next(null, filterArray, includeObject);
  950. },
  951. (filterArray, includeObject, next) => {
  952. const page = payload.page ? payload.page : 1;
  953. const pageSize = 15;
  954. const skipAmount = pageSize * (page - 1);
  955. PlaylistsModule.playlistModel.find({ $or: filterArray }).count((err, count) => {
  956. if (err) next(err);
  957. else {
  958. PlaylistsModule.playlistModel
  959. .find({ $or: filterArray }, includeObject)
  960. .skip(skipAmount)
  961. .limit(pageSize)
  962. .exec((err, playlists) => {
  963. if (err) next(err);
  964. else {
  965. next(null, {
  966. playlists,
  967. page,
  968. pageSize,
  969. skipAmount,
  970. count
  971. });
  972. }
  973. });
  974. }
  975. });
  976. },
  977. (data, next) => {
  978. if (data.playlists.length > 0) next(null, data);
  979. else next("No playlists found");
  980. }
  981. ],
  982. (err, data) => {
  983. if (err && err !== true) return reject(new Error(err));
  984. return resolve(data);
  985. }
  986. )
  987. );
  988. }
  989. /**
  990. * Clears and refills a station playlist
  991. *
  992. * @param {object} payload - object that contains the payload
  993. * @param {string} payload.playlistId - the id of the playlist we are trying to clear and refill
  994. * @returns {Promise} - returns promise (reject, resolve)
  995. */
  996. CLEAR_AND_REFILL_STATION_PLAYLIST(payload) {
  997. return new Promise((resolve, reject) => {
  998. const { playlistId } = payload;
  999. async.waterfall(
  1000. [
  1001. next => {
  1002. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  1003. .then(playlist => {
  1004. next(null, playlist);
  1005. })
  1006. .catch(err => {
  1007. next(err);
  1008. });
  1009. },
  1010. (playlist, next) => {
  1011. if (playlist.type !== "station") next("This playlist is not a station playlist.");
  1012. else next(null, playlist.createdFor);
  1013. },
  1014. (stationId, next) => {
  1015. PlaylistsModule.runJob("AUTOFILL_STATION_PLAYLIST", { stationId }, this)
  1016. .then(() => {
  1017. next();
  1018. })
  1019. .catch(err => {
  1020. next(err);
  1021. });
  1022. }
  1023. ],
  1024. err => {
  1025. if (err && err !== true) return reject(new Error(err));
  1026. return resolve();
  1027. }
  1028. );
  1029. });
  1030. }
  1031. /**
  1032. * Clears and refills a genre playlist
  1033. *
  1034. * @param {object} payload - object that contains the payload
  1035. * @param {string} payload.playlistId - the id of the playlist we are trying to clear and refill
  1036. * @returns {Promise} - returns promise (reject, resolve)
  1037. */
  1038. CLEAR_AND_REFILL_GENRE_PLAYLIST(payload) {
  1039. return new Promise((resolve, reject) => {
  1040. const { playlistId } = payload;
  1041. async.waterfall(
  1042. [
  1043. next => {
  1044. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  1045. .then(playlist => {
  1046. next(null, playlist);
  1047. })
  1048. .catch(err => {
  1049. next(err);
  1050. });
  1051. },
  1052. (playlist, next) => {
  1053. if (playlist.type !== "genre") next("This playlist is not a genre playlist.");
  1054. else next(null, playlist.createdFor);
  1055. },
  1056. (genre, next) => {
  1057. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre }, this)
  1058. .then(() => {
  1059. next();
  1060. })
  1061. .catch(err => {
  1062. next(err);
  1063. });
  1064. }
  1065. ],
  1066. err => {
  1067. if (err && err !== true) return reject(new Error(err));
  1068. return resolve();
  1069. }
  1070. );
  1071. });
  1072. }
  1073. }
  1074. export default new _PlaylistsModule();