songs.js 31 KB

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