songs.js 30 KB

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