songs.js 30 KB

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