playlists.js 29 KB

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