playlists.js 29 KB

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