playlists.js 31 KB

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