playlists.js 31 KB

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