playlists.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185
  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 })
  517. .then(response => {
  518. response.stationIds.forEach(stationId => {
  519. PlaylistsModule.runJob("AUTOFILL_STATION_PLAYLIST", { stationId }).then().catch();
  520. });
  521. })
  522. .catch();
  523. next();
  524. }
  525. ],
  526. err => {
  527. if (err && err !== true) return reject(new Error(err));
  528. return resolve({});
  529. }
  530. );
  531. });
  532. }
  533. /**
  534. * Gets orphaned genre playlists
  535. *
  536. * @returns {Promise} - returns promise (reject, resolve)
  537. */
  538. GET_ORPHANED_GENRE_PLAYLISTS() {
  539. return new Promise((resolve, reject) => {
  540. PlaylistsModule.playlistModel.find({ type: "genre" }, { songs: false }, (err, playlists) => {
  541. if (err) reject(new Error(err));
  542. else {
  543. const orphanedPlaylists = [];
  544. async.eachLimit(
  545. playlists,
  546. 1,
  547. (playlist, next) => {
  548. SongsModule.runJob("GET_ALL_SONGS_WITH_GENRE", { genre: playlist.createdFor }, this)
  549. .then(response => {
  550. if (response.songs.length === 0) {
  551. StationsModule.runJob(
  552. "GET_STATIONS_THAT_INCLUDE_OR_EXCLUDE_PLAYLIST",
  553. { playlistId: playlist._id },
  554. this
  555. )
  556. .then(response => {
  557. if (response.stationIds.length === 0) orphanedPlaylists.push(playlist);
  558. next();
  559. })
  560. .catch(next);
  561. } else next();
  562. })
  563. .catch(next);
  564. },
  565. err => {
  566. if (err) reject(new Error(err));
  567. else resolve({ playlists: orphanedPlaylists });
  568. }
  569. );
  570. }
  571. });
  572. });
  573. }
  574. /**
  575. * Deletes all orphaned genre playlists
  576. *
  577. * @returns {Promise} - returns promise (reject, resolve)
  578. */
  579. DELETE_ORPHANED_GENRE_PLAYLISTS() {
  580. return new Promise((resolve, reject) => {
  581. PlaylistsModule.runJob("GET_ORPHANED_GENRE_PLAYLISTS", {}, this)
  582. .then(response => {
  583. async.eachLimit(
  584. response.playlists,
  585. 1,
  586. (playlist, next) => {
  587. PlaylistsModule.runJob("DELETE_PLAYLIST", { playlistId: playlist._id }, this)
  588. .then(() => {
  589. this.log("INFO", "Deleting orphaned genre playlist");
  590. next();
  591. })
  592. .catch(err => {
  593. next(err);
  594. });
  595. },
  596. err => {
  597. if (err) reject(new Error(err));
  598. else resolve({});
  599. }
  600. );
  601. })
  602. .catch(err => {
  603. reject(new Error(err));
  604. });
  605. });
  606. }
  607. /**
  608. * Gets a orphaned station playlists
  609. *
  610. * @returns {Promise} - returns promise (reject, resolve)
  611. */
  612. GET_ORPHANED_STATION_PLAYLISTS() {
  613. return new Promise((resolve, reject) => {
  614. PlaylistsModule.playlistModel.find({ type: "station" }, { songs: false }, (err, playlists) => {
  615. if (err) reject(new Error(err));
  616. else {
  617. const orphanedPlaylists = [];
  618. async.eachLimit(
  619. playlists,
  620. 1,
  621. (playlist, next) => {
  622. StationsModule.runJob("GET_STATION", { stationId: playlist.createdFor }, this)
  623. .then(station => {
  624. if (station.playlist !== playlist._id.toString()) {
  625. orphanedPlaylists.push(playlist);
  626. }
  627. next();
  628. })
  629. .catch(err => {
  630. if (err.message === "Station not found") {
  631. orphanedPlaylists.push(playlist);
  632. next();
  633. } else next(err);
  634. });
  635. },
  636. err => {
  637. if (err) reject(new Error(err));
  638. else resolve({ playlists: orphanedPlaylists });
  639. }
  640. );
  641. }
  642. });
  643. });
  644. }
  645. /**
  646. * Deletes all orphaned station playlists
  647. *
  648. * @returns {Promise} - returns promise (reject, resolve)
  649. */
  650. DELETE_ORPHANED_STATION_PLAYLISTS() {
  651. return new Promise((resolve, reject) => {
  652. PlaylistsModule.runJob("GET_ORPHANED_STATION_PLAYLISTS", {}, this)
  653. .then(response => {
  654. async.eachLimit(
  655. response.playlists,
  656. 1,
  657. (playlist, next) => {
  658. PlaylistsModule.runJob("DELETE_PLAYLIST", { playlistId: playlist._id }, this)
  659. .then(() => {
  660. this.log("INFO", "Deleting orphaned station playlist");
  661. next();
  662. })
  663. .catch(err => {
  664. next(err);
  665. });
  666. },
  667. err => {
  668. if (err) reject(new Error(err));
  669. else resolve({});
  670. }
  671. );
  672. })
  673. .catch(err => {
  674. reject(new Error(err));
  675. });
  676. });
  677. }
  678. /**
  679. * Fills a station playlist with songs
  680. *
  681. * @param {object} payload - object that contains the payload
  682. * @param {string} payload.stationId - the station id
  683. * @returns {Promise} - returns promise (reject, resolve)
  684. */
  685. AUTOFILL_STATION_PLAYLIST(payload) {
  686. return new Promise((resolve, reject) => {
  687. let originalPlaylist = null;
  688. async.waterfall(
  689. [
  690. next => {
  691. if (!payload.stationId) next("Please specify a station id");
  692. else next();
  693. },
  694. next => {
  695. StationsModule.runJob("GET_STATION", { stationId: payload.stationId }, this)
  696. .then(station => {
  697. next(null, station);
  698. })
  699. .catch(next);
  700. },
  701. (station, next) => {
  702. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId: station.playlist }, this)
  703. .then(playlist => {
  704. originalPlaylist = playlist;
  705. next(null, station);
  706. })
  707. .catch(err => {
  708. next(err);
  709. });
  710. },
  711. (station, next) => {
  712. const includedPlaylists = [];
  713. async.eachLimit(
  714. station.includedPlaylists,
  715. 1,
  716. (playlistId, next) => {
  717. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  718. .then(playlist => {
  719. includedPlaylists.push(playlist);
  720. next();
  721. })
  722. .catch(next);
  723. },
  724. err => {
  725. next(err, station, includedPlaylists);
  726. }
  727. );
  728. },
  729. (station, includedPlaylists, next) => {
  730. const excludedPlaylists = [];
  731. async.eachLimit(
  732. station.excludedPlaylists,
  733. 1,
  734. (playlistId, next) => {
  735. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  736. .then(playlist => {
  737. excludedPlaylists.push(playlist);
  738. next();
  739. })
  740. .catch(next);
  741. },
  742. err => {
  743. next(err, station, includedPlaylists, excludedPlaylists);
  744. }
  745. );
  746. },
  747. (station, includedPlaylists, excludedPlaylists, next) => {
  748. const excludedSongs = excludedPlaylists
  749. .flatMap(excludedPlaylist => excludedPlaylist.songs)
  750. .reduce(
  751. (items, item) =>
  752. items.find(x => x.youtubeId === item.youtubeId) ? [...items] : [...items, item],
  753. []
  754. );
  755. const includedSongs = includedPlaylists
  756. .flatMap(includedPlaylist => includedPlaylist.songs)
  757. .reduce(
  758. (songs, song) =>
  759. songs.find(x => x.youtubeId === song.youtubeId) ? [...songs] : [...songs, song],
  760. []
  761. )
  762. .filter(song => !excludedSongs.find(x => x.youtubeId === song.youtubeId));
  763. next(null, station, includedSongs);
  764. },
  765. (station, includedSongs, next) => {
  766. PlaylistsModule.playlistModel.updateOne(
  767. { _id: station.playlist },
  768. { $set: { songs: includedSongs } },
  769. err => {
  770. next(err, includedSongs);
  771. }
  772. );
  773. },
  774. (includedSongs, next) => {
  775. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId: originalPlaylist._id }, this)
  776. .then(() => {
  777. next(null, includedSongs);
  778. })
  779. .catch(next);
  780. },
  781. (includedSongs, next) => {
  782. if (originalPlaylist.songs.length === 0 && includedSongs.length > 0)
  783. StationsModule.runJob("SKIP_STATION", { stationId: payload.stationId, natural: false });
  784. next();
  785. }
  786. ],
  787. err => {
  788. if (err && err !== true) return reject(new Error(err));
  789. return resolve({});
  790. }
  791. );
  792. });
  793. }
  794. /**
  795. * Gets a playlist by id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
  796. *
  797. * @param {object} payload - object that contains the payload
  798. * @param {string} payload.playlistId - the id of the playlist we are trying to get
  799. * @returns {Promise} - returns promise (reject, resolve)
  800. */
  801. GET_PLAYLIST(payload) {
  802. return new Promise((resolve, reject) =>
  803. async.waterfall(
  804. [
  805. next => {
  806. CacheModule.runJob(
  807. "HGET",
  808. {
  809. table: "playlists",
  810. key: payload.playlistId
  811. },
  812. this
  813. )
  814. .then(playlist => next(null, playlist))
  815. .catch(next);
  816. },
  817. (playlist, next) => {
  818. if (playlist)
  819. PlaylistsModule.playlistModel.exists({ _id: payload.playlistId }, (err, exists) => {
  820. if (err) next(err);
  821. else if (exists) next(null, playlist);
  822. else {
  823. CacheModule.runJob(
  824. "HDEL",
  825. {
  826. table: "playlists",
  827. key: payload.playlistId
  828. },
  829. this
  830. )
  831. .then(() => next())
  832. .catch(next);
  833. }
  834. });
  835. else PlaylistsModule.playlistModel.findOne({ _id: payload.playlistId }, next);
  836. },
  837. (playlist, next) => {
  838. if (playlist) {
  839. CacheModule.runJob(
  840. "HSET",
  841. {
  842. table: "playlists",
  843. key: payload.playlistId,
  844. value: playlist
  845. },
  846. this
  847. )
  848. .then(playlist => {
  849. next(null, playlist);
  850. })
  851. .catch(next);
  852. } else next("Playlist not found");
  853. }
  854. ],
  855. (err, playlist) => {
  856. if (err && err !== true) return reject(new Error(err));
  857. return resolve(playlist);
  858. }
  859. )
  860. );
  861. }
  862. /**
  863. * Gets a playlist from id from Mongo and updates the cache with it
  864. *
  865. * @param {object} payload - object that contains the payload
  866. * @param {string} payload.playlistId - the id of the playlist we are trying to update
  867. * @returns {Promise} - returns promise (reject, resolve)
  868. */
  869. UPDATE_PLAYLIST(payload) {
  870. return new Promise((resolve, reject) =>
  871. async.waterfall(
  872. [
  873. next => {
  874. PlaylistsModule.playlistModel.findOne({ _id: payload.playlistId }, next);
  875. },
  876. (playlist, next) => {
  877. if (!playlist) {
  878. CacheModule.runJob("HDEL", {
  879. table: "playlists",
  880. key: payload.playlistId
  881. });
  882. return next("Playlist not found");
  883. }
  884. return CacheModule.runJob(
  885. "HSET",
  886. {
  887. table: "playlists",
  888. key: payload.playlistId,
  889. value: playlist
  890. },
  891. this
  892. )
  893. .then(playlist => {
  894. next(null, playlist);
  895. })
  896. .catch(next);
  897. }
  898. ],
  899. (err, playlist) => {
  900. if (err && err !== true) return reject(new Error(err));
  901. return resolve(playlist);
  902. }
  903. )
  904. );
  905. }
  906. /**
  907. * Deletes playlist from id from Mongo and cache
  908. *
  909. * @param {object} payload - object that contains the payload
  910. * @param {string} payload.playlistId - the id of the playlist we are trying to delete
  911. * @returns {Promise} - returns promise (reject, resolve)
  912. */
  913. DELETE_PLAYLIST(payload) {
  914. return new Promise((resolve, reject) =>
  915. async.waterfall(
  916. [
  917. next => {
  918. PlaylistsModule.playlistModel.deleteOne({ _id: payload.playlistId }, next);
  919. },
  920. (res, next) => {
  921. CacheModule.runJob(
  922. "HDEL",
  923. {
  924. table: "playlists",
  925. key: payload.playlistId
  926. },
  927. this
  928. )
  929. .then(() => next())
  930. .catch(next);
  931. }
  932. ],
  933. err => {
  934. if (err && err !== true) return reject(new Error(err));
  935. return resolve();
  936. }
  937. )
  938. );
  939. }
  940. /**
  941. * Searches through playlists
  942. *
  943. * @param {object} payload - object that contains the payload
  944. * @param {string} payload.query - the query
  945. * @param {string} payload.includePrivate - include private playlists
  946. * @param {string} payload.includeStation - include station playlists
  947. * @param {string} payload.includeUser - include user playlists
  948. * @param {string} payload.includeGenre - include genre playlists
  949. * @param {string} payload.includeOwn - include own user playlists
  950. * @param {string} payload.userId - the user id of the person requesting
  951. * @param {string} payload.includeSongs - include songs
  952. * @param {string} payload.page - page (default 1)
  953. * @returns {Promise} - returns promise (reject, resolve)
  954. */
  955. SEARCH(payload) {
  956. return new Promise((resolve, reject) =>
  957. async.waterfall(
  958. [
  959. next => {
  960. const types = [];
  961. if (payload.includeStation) types.push("station");
  962. if (payload.includeUser) types.push("user");
  963. if (payload.includeGenre) types.push("genre");
  964. if (types.length === 0 && !payload.includeOwn) return next("No types have been included.");
  965. const privacies = ["public"];
  966. if (payload.includePrivate) privacies.push("private");
  967. const includeObject = payload.includeSongs ? null : { songs: false };
  968. const filterArray = [
  969. {
  970. displayName: new RegExp(`${payload.query}`, "i"),
  971. privacy: { $in: privacies },
  972. type: { $in: types }
  973. }
  974. ];
  975. if (payload.includeOwn && payload.userId)
  976. filterArray.push({
  977. displayName: new RegExp(`${payload.query}`, "i"),
  978. type: "user",
  979. createdBy: payload.userId
  980. });
  981. return next(null, filterArray, includeObject);
  982. },
  983. (filterArray, includeObject, next) => {
  984. const page = payload.page ? payload.page : 1;
  985. const pageSize = 15;
  986. const skipAmount = pageSize * (page - 1);
  987. PlaylistsModule.playlistModel.find({ $or: filterArray }).count((err, count) => {
  988. if (err) next(err);
  989. else {
  990. PlaylistsModule.playlistModel
  991. .find({ $or: filterArray }, includeObject)
  992. .skip(skipAmount)
  993. .limit(pageSize)
  994. .exec((err, playlists) => {
  995. if (err) next(err);
  996. else {
  997. next(null, {
  998. playlists,
  999. page,
  1000. pageSize,
  1001. skipAmount,
  1002. count
  1003. });
  1004. }
  1005. });
  1006. }
  1007. });
  1008. },
  1009. (data, next) => {
  1010. if (data.playlists.length > 0) next(null, data);
  1011. else next("No playlists found");
  1012. }
  1013. ],
  1014. (err, data) => {
  1015. if (err && err !== true) return reject(new Error(err));
  1016. return resolve(data);
  1017. }
  1018. )
  1019. );
  1020. }
  1021. /**
  1022. * Clears and refills a station 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_STATION_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 !== "station") next("This playlist is not a station playlist.");
  1044. else next(null, playlist.createdFor);
  1045. },
  1046. (stationId, next) => {
  1047. PlaylistsModule.runJob("AUTOFILL_STATION_PLAYLIST", { stationId }, 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. * Clears and refills a genre playlist
  1065. *
  1066. * @param {object} payload - object that contains the payload
  1067. * @param {string} payload.playlistId - the id of the playlist we are trying to clear and refill
  1068. * @returns {Promise} - returns promise (reject, resolve)
  1069. */
  1070. CLEAR_AND_REFILL_GENRE_PLAYLIST(payload) {
  1071. return new Promise((resolve, reject) => {
  1072. const { playlistId } = payload;
  1073. async.waterfall(
  1074. [
  1075. next => {
  1076. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  1077. .then(playlist => {
  1078. next(null, playlist);
  1079. })
  1080. .catch(err => {
  1081. next(err);
  1082. });
  1083. },
  1084. (playlist, next) => {
  1085. if (playlist.type !== "genre") next("This playlist is not a genre playlist.");
  1086. else next(null, playlist.createdFor);
  1087. },
  1088. (genre, next) => {
  1089. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre }, this)
  1090. .then(() => {
  1091. next();
  1092. })
  1093. .catch(err => {
  1094. next(err);
  1095. });
  1096. }
  1097. ],
  1098. err => {
  1099. if (err && err !== true) return reject(new Error(err));
  1100. return resolve();
  1101. }
  1102. );
  1103. });
  1104. }
  1105. }
  1106. export default new _PlaylistsModule();