songs.js 31 KB

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