playlists.js 31 KB

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