songs.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  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. class _SongsModule extends CoreClass {
  9. // eslint-disable-next-line require-jsdoc
  10. constructor() {
  11. super("songs");
  12. SongsModule = this;
  13. }
  14. /**
  15. * Initialises the songs module
  16. *
  17. * @returns {Promise} - returns promise (reject, resolve)
  18. */
  19. async initialize() {
  20. this.setStage(1);
  21. CacheModule = this.moduleManager.modules.cache;
  22. DBModule = this.moduleManager.modules.db;
  23. UtilsModule = this.moduleManager.modules.utils;
  24. this.songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" });
  25. this.songSchemaCache = await CacheModule.runJob("GET_SCHEMA", { schemaName: "song" });
  26. this.setStage(2);
  27. return new Promise((resolve, reject) =>
  28. async.waterfall(
  29. [
  30. next => {
  31. this.setStage(2);
  32. CacheModule.runJob("HGETALL", { table: "songs" })
  33. .then(songs => {
  34. next(null, songs);
  35. })
  36. .catch(next);
  37. },
  38. (songs, next) => {
  39. this.setStage(3);
  40. if (!songs) return next();
  41. const songIds = Object.keys(songs);
  42. return async.each(
  43. songIds,
  44. (songId, next) => {
  45. SongsModule.songModel.findOne({ songId }, (err, song) => {
  46. if (err) next(err);
  47. else if (!song)
  48. CacheModule.runJob("HDEL", {
  49. table: "songs",
  50. key: songId
  51. })
  52. .then(() => next())
  53. .catch(next);
  54. else next();
  55. });
  56. },
  57. next
  58. );
  59. },
  60. next => {
  61. this.setStage(4);
  62. SongsModule.songModel.find({}, next);
  63. },
  64. (songs, next) => {
  65. this.setStage(5);
  66. async.each(
  67. songs,
  68. (song, next) => {
  69. CacheModule.runJob("HSET", {
  70. table: "songs",
  71. key: song.songId,
  72. value: SongsModule.songSchemaCache(song)
  73. })
  74. .then(() => next())
  75. .catch(next);
  76. },
  77. next
  78. );
  79. }
  80. ],
  81. async err => {
  82. if (err) {
  83. err = await UtilsModule.runJob("GET_ERROR", { error: err });
  84. reject(new Error(err));
  85. } else resolve();
  86. }
  87. )
  88. );
  89. }
  90. /**
  91. * Gets a song by id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
  92. *
  93. * @param {object} payload - object containing the payload
  94. * @param {string} payload.id - the id of the song we are trying to get
  95. * @returns {Promise} - returns a promise (resolve, reject)
  96. */
  97. GET_SONG(payload) {
  98. return new Promise((resolve, reject) =>
  99. async.waterfall(
  100. [
  101. next => {
  102. console.log(payload);
  103. if (!mongoose.Types.ObjectId.isValid(payload.id)) return next("Id is not a valid ObjectId.");
  104. return CacheModule.runJob("HGET", { table: "songs", key: payload.id }, this)
  105. .then(song => next(null, song))
  106. .catch(next);
  107. },
  108. (song, next) => {
  109. if (song) return next(true, song);
  110. return SongsModule.songModel.findOne({ _id: payload.id }, next);
  111. },
  112. (song, next) => {
  113. if (song) {
  114. CacheModule.runJob(
  115. "HSET",
  116. {
  117. table: "songs",
  118. key: payload.id,
  119. value: song
  120. },
  121. this
  122. ).then(song => next(null, song));
  123. } else next("Song not found.");
  124. }
  125. ],
  126. (err, song) => {
  127. if (err && err !== true) return reject(new Error(err));
  128. return resolve({ song });
  129. }
  130. )
  131. );
  132. }
  133. /**
  134. * Gets a song by song id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
  135. *
  136. * @param {object} payload - an object containing the payload
  137. * @param {string} payload.songId - the mongo id of the song we are trying to get
  138. * @returns {Promise} - returns a promise (resolve, reject)
  139. */
  140. GET_SONG_FROM_ID(payload) {
  141. return new Promise((resolve, reject) =>
  142. async.waterfall(
  143. [
  144. next => {
  145. SongsModule.songModel.findOne({ songId: payload.songId }, next);
  146. }
  147. ],
  148. (err, song) => {
  149. if (err && err !== true) return reject(new Error(err));
  150. return resolve({ song });
  151. }
  152. )
  153. );
  154. }
  155. /**
  156. * Gets a song from id from Mongo and updates the cache with it
  157. *
  158. * @param {object} payload - an object containing the payload
  159. * @param {string} payload.songId - the id of the song we are trying to update
  160. * @returns {Promise} - returns a promise (resolve, reject)
  161. */
  162. UPDATE_SONG(payload) {
  163. // songId, cb
  164. return new Promise((resolve, reject) =>
  165. async.waterfall(
  166. [
  167. next => {
  168. SongsModule.songModel.findOne({ _id: payload.songId }, next);
  169. },
  170. (song, next) => {
  171. if (!song) {
  172. CacheModule.runJob("HDEL", {
  173. table: "songs",
  174. key: payload.songId
  175. });
  176. return next("Song not found.");
  177. }
  178. return CacheModule.runJob(
  179. "HSET",
  180. {
  181. table: "songs",
  182. key: payload.songId,
  183. value: song
  184. },
  185. this
  186. )
  187. .then(song => {
  188. next(null, song);
  189. })
  190. .catch(next);
  191. }
  192. ],
  193. (err, song) => {
  194. if (err && err !== true) return reject(new Error(err));
  195. return resolve(song);
  196. }
  197. )
  198. );
  199. }
  200. /**
  201. * Deletes song from id from Mongo and cache
  202. *
  203. * @param {object} payload - returns an object containing the payload
  204. * @param {string} payload.songId - the id of the song we are trying to delete
  205. * @returns {Promise} - returns a promise (resolve, reject)
  206. */
  207. DELETE_SONG(payload) {
  208. return new Promise((resolve, reject) =>
  209. async.waterfall(
  210. [
  211. next => {
  212. SongsModule.songModel.deleteOne({ songId: payload.songId }, next);
  213. },
  214. next => {
  215. CacheModule.runJob(
  216. "HDEL",
  217. {
  218. table: "songs",
  219. key: payload.songId
  220. },
  221. this
  222. )
  223. .then(() => next())
  224. .catch(next);
  225. }
  226. ],
  227. err => {
  228. if (err && err !== true) return reject(new Error(err));
  229. return resolve();
  230. }
  231. )
  232. );
  233. }
  234. /**
  235. * Recalculates dislikes and likes for a song
  236. *
  237. * @param {object} payload - returns an object containing the payload
  238. * @param {string} payload.musareSongId - the (musare) id of the song
  239. * @param {string} payload.songId - the (mongodb) id of the song
  240. * @returns {Promise} - returns a promise (resolve, reject)
  241. */
  242. async RECALCULATE_SONG_RATINGS(payload) {
  243. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  244. return new Promise((resolve, reject) => {
  245. async.waterfall(
  246. [
  247. next => {
  248. playlistModel.countDocuments(
  249. { songs: { $elemMatch: { songId: payload.musareSongId } }, displayName: "Liked Songs" },
  250. (err, likes) => {
  251. if (err) return next(err);
  252. return next(null, likes);
  253. }
  254. );
  255. },
  256. (likes, next) => {
  257. playlistModel.countDocuments(
  258. { songs: { $elemMatch: { songId: payload.musareSongId } }, displayName: "Disliked Songs" },
  259. (err, dislikes) => {
  260. if (err) return next(err);
  261. return next(err, { likes, dislikes });
  262. }
  263. );
  264. },
  265. ({ likes, dislikes }, next) => {
  266. SongsModule.songModel.updateOne(
  267. { _id: payload.songId },
  268. {
  269. $set: {
  270. likes,
  271. dislikes
  272. }
  273. },
  274. err => next(err, { likes, dislikes })
  275. );
  276. }
  277. ],
  278. (err, { likes, dislikes }) => {
  279. if (err) return reject(new Error(err));
  280. return resolve({ likes, dislikes });
  281. }
  282. );
  283. });
  284. }
  285. /**
  286. * Gets an array of all genres
  287. *
  288. * @returns {Promise} - returns a promise (resolve, reject)
  289. */
  290. GET_ALL_GENRES() {
  291. return new Promise((resolve, reject) =>
  292. async.waterfall(
  293. [
  294. next => {
  295. SongsModule.songModel.find({}, { genres: 1, _id: false }, next);
  296. },
  297. (songs, next) => {
  298. let allGenres = [];
  299. songs.forEach(song => {
  300. allGenres = allGenres.concat(song.genres);
  301. });
  302. const lowerCaseGenres = allGenres.map(genre => genre.toLowerCase());
  303. const uniqueGenres = lowerCaseGenres.filter(
  304. (value, index, self) => self.indexOf(value) === index
  305. );
  306. next(null, uniqueGenres);
  307. }
  308. ],
  309. (err, genres) => {
  310. if (err && err !== true) return reject(new Error(err));
  311. return resolve({ genres });
  312. }
  313. )
  314. );
  315. }
  316. /**
  317. * Gets an array of all songs with a specific genre
  318. *
  319. * @param {object} payload - returns an object containing the payload
  320. * @param {string} payload.genre - the genre
  321. * @returns {Promise} - returns a promise (resolve, reject)
  322. */
  323. GET_ALL_SONGS_WITH_GENRE(payload) {
  324. return new Promise((resolve, reject) =>
  325. async.waterfall(
  326. [
  327. next => {
  328. SongsModule.songModel.find(
  329. { genres: { $regex: new RegExp(`^${payload.genre.toLowerCase()}$`, "i") } },
  330. next
  331. );
  332. }
  333. ],
  334. (err, songs) => {
  335. if (err && err !== true) return reject(new Error(err));
  336. return resolve({ songs });
  337. }
  338. )
  339. );
  340. }
  341. }
  342. export default new _SongsModule();