songs.js 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148
  1. import async from "async";
  2. import { isAdminRequired, isLoginRequired } from "./hooks";
  3. // eslint-disable-next-line
  4. import moduleManager from "../../index";
  5. const DBModule = moduleManager.modules.db;
  6. const UtilsModule = moduleManager.modules.utils;
  7. const WSModule = moduleManager.modules.ws;
  8. const CacheModule = moduleManager.modules.cache;
  9. const SongsModule = moduleManager.modules.songs;
  10. const ActivitiesModule = moduleManager.modules.activities;
  11. const YouTubeModule = moduleManager.modules.youtube;
  12. const PlaylistsModule = moduleManager.modules.playlists;
  13. const StationsModule = moduleManager.modules.stations;
  14. CacheModule.runJob("SUB", {
  15. channel: "song.updated",
  16. cb: async data => {
  17. const songModel = await DBModule.runJob("GET_MODEL", {
  18. modelName: "song"
  19. });
  20. songModel.findOne({ _id: data.songId }, (err, song) => {
  21. WSModule.runJob("EMIT_TO_ROOMS", {
  22. rooms: ["import-album", "admin.songs", `edit-song.${data.songId}`, "edit-songs"],
  23. args: ["event:admin.song.updated", { data: { song, oldStatus: data.oldStatus } }]
  24. });
  25. });
  26. }
  27. });
  28. CacheModule.runJob("SUB", {
  29. channel: "song.removed",
  30. cb: async data => {
  31. WSModule.runJob("EMIT_TO_ROOMS", {
  32. rooms: ["import-album", "admin.songs", `edit-song.${data.songId}`, "edit-songs"],
  33. args: ["event:admin.song.removed", { data }]
  34. });
  35. }
  36. });
  37. CacheModule.runJob("SUB", {
  38. channel: "song.like",
  39. cb: data => {
  40. WSModule.runJob("EMIT_TO_ROOM", {
  41. room: `song.${data.youtubeId}`,
  42. args: [
  43. "event:song.liked",
  44. {
  45. data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes }
  46. }
  47. ]
  48. });
  49. WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
  50. sockets.forEach(socket => {
  51. socket.dispatch("event:song.ratings.updated", {
  52. data: {
  53. youtubeId: data.youtubeId,
  54. liked: true,
  55. disliked: false
  56. }
  57. });
  58. });
  59. });
  60. }
  61. });
  62. CacheModule.runJob("SUB", {
  63. channel: "song.dislike",
  64. cb: data => {
  65. WSModule.runJob("EMIT_TO_ROOM", {
  66. room: `song.${data.youtubeId}`,
  67. args: [
  68. "event:song.disliked",
  69. {
  70. data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes }
  71. }
  72. ]
  73. });
  74. WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
  75. sockets.forEach(socket => {
  76. socket.dispatch("event:song.ratings.updated", {
  77. data: {
  78. youtubeId: data.youtubeId,
  79. liked: false,
  80. disliked: true
  81. }
  82. });
  83. });
  84. });
  85. }
  86. });
  87. CacheModule.runJob("SUB", {
  88. channel: "song.unlike",
  89. cb: data => {
  90. WSModule.runJob("EMIT_TO_ROOM", {
  91. room: `song.${data.youtubeId}`,
  92. args: [
  93. "event:song.unliked",
  94. {
  95. data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes }
  96. }
  97. ]
  98. });
  99. WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
  100. sockets.forEach(socket => {
  101. socket.dispatch("event:song.ratings.updated", {
  102. data: {
  103. youtubeId: data.youtubeId,
  104. liked: false,
  105. disliked: false
  106. }
  107. });
  108. });
  109. });
  110. }
  111. });
  112. CacheModule.runJob("SUB", {
  113. channel: "song.undislike",
  114. cb: data => {
  115. WSModule.runJob("EMIT_TO_ROOM", {
  116. room: `song.${data.youtubeId}`,
  117. args: [
  118. "event:song.undisliked",
  119. {
  120. data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes }
  121. }
  122. ]
  123. });
  124. WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
  125. sockets.forEach(socket => {
  126. socket.dispatch("event:song.ratings.updated", {
  127. data: {
  128. youtubeId: data.youtubeId,
  129. liked: false,
  130. disliked: false
  131. }
  132. });
  133. });
  134. });
  135. }
  136. });
  137. export default {
  138. /**
  139. * Returns the length of the songs list
  140. *
  141. * @param {object} session - the session object automatically added by the websocket
  142. * @param cb
  143. */
  144. length: isAdminRequired(async function length(session, cb) {
  145. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  146. async.waterfall(
  147. [
  148. next => {
  149. songModel.countDocuments({}, next);
  150. }
  151. ],
  152. async (err, count) => {
  153. if (err) {
  154. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  155. this.log("ERROR", "SONGS_LENGTH", `Failed to get length from songs. "${err}"`);
  156. return cb({ status: "error", message: err });
  157. }
  158. this.log("SUCCESS", "SONGS_LENGTH", `Got length from songs successfully.`);
  159. return cb({ status: "success", message: "Successfully got length of songs.", data: { length: count } });
  160. }
  161. );
  162. }),
  163. /**
  164. * Gets songs, used in the admin songs page by the AdvancedTable component
  165. *
  166. * @param {object} session - the session object automatically added by the websocket
  167. * @param page - the page
  168. * @param pageSize - the size per page
  169. * @param properties - the properties to return for each song
  170. * @param sort - the sort object
  171. * @param queries - the queries array
  172. * @param operator - the operator for queries
  173. * @param cb
  174. */
  175. getData: isAdminRequired(async function getSet(session, page, pageSize, properties, sort, queries, operator, cb) {
  176. async.waterfall(
  177. [
  178. next => {
  179. DBModule.runJob(
  180. "GET_DATA",
  181. {
  182. page,
  183. pageSize,
  184. properties,
  185. sort,
  186. queries,
  187. operator,
  188. modelName: "song",
  189. blacklistedProperties: [],
  190. specialProperties: {
  191. requestedBy: [
  192. {
  193. $addFields: {
  194. requestedByOID: {
  195. $convert: {
  196. input: "$requestedBy",
  197. to: "objectId",
  198. onError: "unknown",
  199. onNull: "unknown"
  200. }
  201. }
  202. }
  203. },
  204. {
  205. $lookup: {
  206. from: "users",
  207. localField: "requestedByOID",
  208. foreignField: "_id",
  209. as: "requestedByUser"
  210. }
  211. },
  212. {
  213. $addFields: {
  214. requestedByUsername: {
  215. $ifNull: ["$requestedByUser.username", "unknown"]
  216. }
  217. }
  218. },
  219. {
  220. $project: {
  221. requestedByOID: 0,
  222. requestedByUser: 0
  223. }
  224. }
  225. ],
  226. verifiedBy: [
  227. {
  228. $addFields: {
  229. verifiedByOID: {
  230. $convert: {
  231. input: "$verifiedBy",
  232. to: "objectId",
  233. onError: "unknown",
  234. onNull: "unknown"
  235. }
  236. }
  237. }
  238. },
  239. {
  240. $lookup: {
  241. from: "users",
  242. localField: "verifiedByOID",
  243. foreignField: "_id",
  244. as: "verifiedByUser"
  245. }
  246. },
  247. {
  248. $unwind: {
  249. path: "$verifiedByUser",
  250. preserveNullAndEmptyArrays: true
  251. }
  252. },
  253. {
  254. $addFields: {
  255. verifiedByUsername: {
  256. $ifNull: ["$verifiedByUser.username", "unknown"]
  257. }
  258. }
  259. },
  260. {
  261. $project: {
  262. verifiedByOID: 0,
  263. verifiedByUser: 0
  264. }
  265. }
  266. ]
  267. },
  268. specialQueries: {
  269. requestedBy: newQuery => ({
  270. $or: [newQuery, { requestedByUsername: newQuery.requestedBy }]
  271. }),
  272. verifiedBy: newQuery => ({
  273. $or: [newQuery, { verifiedByUsername: newQuery.verifiedBy }]
  274. })
  275. }
  276. },
  277. this
  278. )
  279. .then(response => {
  280. next(null, response);
  281. })
  282. .catch(err => {
  283. next(err);
  284. });
  285. }
  286. ],
  287. async (err, response) => {
  288. if (err) {
  289. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  290. this.log("ERROR", "SONGS_GET_DATA", `Failed to get data from songs. "${err}"`);
  291. return cb({ status: "error", message: err });
  292. }
  293. this.log("SUCCESS", "SONGS_GET_DATA", `Got data from songs successfully.`);
  294. return cb({ status: "success", message: "Successfully got data from songs.", data: response });
  295. }
  296. );
  297. }),
  298. /**
  299. * Updates all songs
  300. *
  301. * @param {object} session - the session object automatically added by the websocket
  302. * @param cb
  303. */
  304. updateAll: isAdminRequired(async function updateAll(session, cb) {
  305. async.waterfall(
  306. [
  307. next => {
  308. SongsModule.runJob("UPDATE_ALL_SONGS", {}, this)
  309. .then(() => {
  310. next();
  311. })
  312. .catch(err => {
  313. next(err);
  314. });
  315. }
  316. ],
  317. async err => {
  318. if (err) {
  319. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  320. this.log("ERROR", "SONGS_UPDATE_ALL", `Failed to update all songs. "${err}"`);
  321. return cb({ status: "error", message: err });
  322. }
  323. this.log("SUCCESS", "SONGS_UPDATE_ALL", `Updated all songs successfully.`);
  324. return cb({ status: "success", message: "Successfully updated all songs." });
  325. }
  326. );
  327. }),
  328. /**
  329. * Recalculates all song ratings
  330. *
  331. * @param {object} session - the session object automatically added by the websocket
  332. * @param cb
  333. */
  334. recalculateAllRatings: isAdminRequired(async function recalculateAllRatings(session, cb) {
  335. async.waterfall(
  336. [
  337. next => {
  338. SongsModule.runJob("RECALCULATE_ALL_SONG_RATINGS", {}, this)
  339. .then(() => {
  340. next();
  341. })
  342. .catch(err => {
  343. next(err);
  344. });
  345. }
  346. ],
  347. async err => {
  348. if (err) {
  349. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  350. this.log(
  351. "ERROR",
  352. "SONGS_RECALCULATE_ALL_RATINGS",
  353. `Failed to recalculate all song ratings. "${err}"`
  354. );
  355. return cb({ status: "error", message: err });
  356. }
  357. this.log("SUCCESS", "SONGS_RECALCULATE_ALL_RATINGS", `Recalculated all song ratings successfully.`);
  358. return cb({ status: "success", message: "Successfully recalculated all song ratings." });
  359. }
  360. );
  361. }),
  362. /**
  363. * Gets a song from the Musare song id
  364. *
  365. * @param {object} session - the session object automatically added by the websocket
  366. * @param {string} songId - the song id
  367. * @param {Function} cb
  368. */
  369. getSongFromSongId: isAdminRequired(function getSongFromSongId(session, songId, cb) {
  370. async.waterfall(
  371. [
  372. next => {
  373. SongsModule.runJob("GET_SONG", { songId }, this)
  374. .then(response => next(null, response.song))
  375. .catch(err => next(err));
  376. }
  377. ],
  378. async (err, song) => {
  379. if (err) {
  380. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  381. this.log("ERROR", "SONGS_GET_SONG_FROM_MUSARE_ID", `Failed to get song ${songId}. "${err}"`);
  382. return cb({ status: "error", message: err });
  383. }
  384. this.log("SUCCESS", "SONGS_GET_SONG_FROM_MUSARE_ID", `Got song ${songId} successfully.`);
  385. return cb({ status: "success", data: { song } });
  386. }
  387. );
  388. }),
  389. /**
  390. * Gets multiple songs from the Musare song ids
  391. * At this time only used in EditSongs
  392. *
  393. * @param {object} session - the session object automatically added by the websocket
  394. * @param {Array} songIds - the song ids
  395. * @param {Function} cb
  396. */
  397. getSongsFromSongIds: isAdminRequired(function getSongFromSongId(session, songIds, cb) {
  398. async.waterfall(
  399. [
  400. next => {
  401. SongsModule.runJob(
  402. "GET_SONGS",
  403. {
  404. songIds,
  405. properties: ["youtubeId", "title", "artists", "thumbnail", "duration", "verified", "_id"]
  406. },
  407. this
  408. )
  409. .then(response => next(null, response.songs))
  410. .catch(err => next(err));
  411. }
  412. ],
  413. async (err, songs) => {
  414. if (err) {
  415. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  416. this.log("ERROR", "SONGS_GET_SONGS_FROM_MUSARE_IDS", `Failed to get songs. "${err}"`);
  417. return cb({ status: "error", message: err });
  418. }
  419. this.log("SUCCESS", "SONGS_GET_SONGS_FROM_MUSARE_IDS", `Got songs successfully.`);
  420. return cb({ status: "success", data: { songs } });
  421. }
  422. );
  423. }),
  424. /**
  425. * Creates a song
  426. *
  427. * @param {object} session - the session object automatically added by the websocket
  428. * @param {object} newSong - the song object
  429. * @param {Function} cb
  430. */
  431. create: isAdminRequired(async function create(session, newSong, cb) {
  432. async.waterfall(
  433. [
  434. next => {
  435. SongsModule.runJob("CREATE_SONG", { song: newSong, userId: session.userId }, this)
  436. .then(song => next(null, song))
  437. .catch(next);
  438. }
  439. ],
  440. async (err, song) => {
  441. if (err) {
  442. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  443. this.log("ERROR", "SONGS_CREATE", `Failed to create song "${JSON.stringify(newSong)}". "${err}"`);
  444. return cb({ status: "error", message: err });
  445. }
  446. this.log("SUCCESS", "SONGS_CREATE", `Successfully created song "${song._id}".`);
  447. return cb({
  448. status: "success",
  449. message: "Song has been successfully created",
  450. data: { song }
  451. });
  452. }
  453. );
  454. }),
  455. /**
  456. * Updates a song
  457. *
  458. * @param {object} session - the session object automatically added by the websocket
  459. * @param {string} songId - the song id
  460. * @param {object} song - the updated song object
  461. * @param {Function} cb
  462. */
  463. update: isAdminRequired(async function update(session, songId, song, cb) {
  464. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  465. let existingSong = null;
  466. async.waterfall(
  467. [
  468. next => {
  469. songModel.findOne({ _id: songId }, next);
  470. },
  471. (_existingSong, next) => {
  472. existingSong = _existingSong;
  473. // Verify the song
  474. if (existingSong.verified === false && song.verified === true) {
  475. song.verifiedBy = session.userId;
  476. song.verifiedAt = Date.now();
  477. }
  478. // Unverify the song
  479. else if (existingSong.verified === true && song.verified === false) {
  480. song.verifiedBy = null;
  481. song.verifiedAt = null;
  482. }
  483. next();
  484. },
  485. next => {
  486. songModel.updateOne({ _id: songId }, song, { runValidators: true }, next);
  487. },
  488. (res, next) => {
  489. SongsModule.runJob("UPDATE_SONG", { songId }, this)
  490. .then(song => {
  491. existingSong.genres
  492. .concat(song.genres)
  493. .filter((value, index, self) => self.indexOf(value) === index)
  494. .forEach(genre => {
  495. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", {
  496. genre,
  497. createPlaylist: song.verified
  498. })
  499. .then(() => {})
  500. .catch(() => {});
  501. });
  502. existingSong.artists
  503. .concat(song.artists)
  504. .filter((value, index, self) => self.indexOf(value) === index)
  505. .forEach(artist => {
  506. PlaylistsModule.runJob("AUTOFILL_ARTIST_PLAYLIST", { artist })
  507. .then(() => {})
  508. .catch(() => {});
  509. });
  510. next(null, song);
  511. })
  512. .catch(next);
  513. }
  514. ],
  515. async (err, song) => {
  516. if (err) {
  517. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  518. this.log("ERROR", "SONGS_UPDATE", `Failed to update song "${songId}". "${err}"`);
  519. return cb({ status: "error", message: err });
  520. }
  521. this.log("SUCCESS", "SONGS_UPDATE", `Successfully updated song "${songId}".`);
  522. return cb({
  523. status: "success",
  524. message: "Song has been successfully updated",
  525. data: { song }
  526. });
  527. }
  528. );
  529. }),
  530. /**
  531. * Removes a song
  532. *
  533. * @param session
  534. * @param songId - the song id
  535. * @param cb
  536. */
  537. remove: isAdminRequired(async function remove(session, songId, cb) {
  538. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  539. const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
  540. async.waterfall(
  541. [
  542. next => {
  543. songModel.findOne({ _id: songId }, next);
  544. },
  545. (song, next) => {
  546. PlaylistsModule.runJob("GET_PLAYLISTS_WITH_SONG", { songId }, this)
  547. .then(res => {
  548. async.eachLimit(
  549. res.playlists,
  550. 1,
  551. (playlist, next) => {
  552. WSModule.runJob(
  553. "RUN_ACTION2",
  554. {
  555. session,
  556. namespace: "playlists",
  557. action: "removeSongFromPlaylist",
  558. args: [song.youtubeId, playlist._id]
  559. },
  560. this
  561. )
  562. .then(res => {
  563. if (res.status === "error") next(res.message);
  564. else next();
  565. })
  566. .catch(err => {
  567. next(err);
  568. });
  569. },
  570. err => {
  571. if (err) next(err);
  572. else next(null, song);
  573. }
  574. );
  575. })
  576. .catch(err => next(err));
  577. },
  578. (song, next) => {
  579. stationModel.find({ "queue._id": songId }, (err, stations) => {
  580. if (err) next(err);
  581. else {
  582. async.eachLimit(
  583. stations,
  584. 1,
  585. (station, next) => {
  586. WSModule.runJob(
  587. "RUN_ACTION2",
  588. {
  589. session,
  590. namespace: "stations",
  591. action: "removeFromQueue",
  592. args: [station._id, song.youtubeId]
  593. },
  594. this
  595. )
  596. .then(res => {
  597. if (
  598. res.status === "error" &&
  599. res.message !== "Station not found" &&
  600. res.message !== "Song is not currently in the queue."
  601. )
  602. next(res.message);
  603. else next();
  604. })
  605. .catch(err => {
  606. next(err);
  607. });
  608. },
  609. err => {
  610. if (err) next(err);
  611. else next();
  612. }
  613. );
  614. }
  615. });
  616. },
  617. next => {
  618. stationModel.find({ "currentSong._id": songId }, (err, stations) => {
  619. if (err) next(err);
  620. else {
  621. async.eachLimit(
  622. stations,
  623. 1,
  624. (station, next) => {
  625. StationsModule.runJob(
  626. "SKIP_STATION",
  627. { stationId: station._id, natural: false },
  628. this
  629. )
  630. .then(() => {
  631. next();
  632. })
  633. .catch(err => {
  634. if (err.message === "Station not found.") next();
  635. else next(err);
  636. });
  637. },
  638. err => {
  639. if (err) next(err);
  640. else next();
  641. }
  642. );
  643. }
  644. });
  645. },
  646. next => {
  647. songModel.deleteOne({ _id: songId }, err => {
  648. if (err) next(err);
  649. else next();
  650. });
  651. },
  652. next => {
  653. CacheModule.runJob("HDEL", { table: "songs", key: songId }, this)
  654. .then(() => {
  655. next();
  656. })
  657. .catch(next);
  658. }
  659. ],
  660. async err => {
  661. if (err) {
  662. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  663. this.log("ERROR", "SONGS_REMOVE", `Failed to remove song "${songId}". "${err}"`);
  664. return cb({ status: "error", message: err });
  665. }
  666. this.log("SUCCESS", "SONGS_REMOVE", `Successfully removed song "${songId}".`);
  667. CacheModule.runJob("PUB", {
  668. channel: "song.removed",
  669. value: { songId }
  670. });
  671. return cb({
  672. status: "success",
  673. message: "Song has been successfully removed"
  674. });
  675. }
  676. );
  677. }),
  678. /**
  679. * Removes many songs
  680. *
  681. * @param session
  682. * @param songIds - array of song ids
  683. * @param cb
  684. */
  685. removeMany: isAdminRequired(async function remove(session, songIds, cb) {
  686. const successful = [];
  687. const failed = [];
  688. async.waterfall(
  689. [
  690. next => {
  691. async.eachLimit(
  692. songIds,
  693. 1,
  694. (songId, next) => {
  695. WSModule.runJob(
  696. "RUN_ACTION2",
  697. {
  698. session,
  699. namespace: "songs",
  700. action: "remove",
  701. args: [songId]
  702. },
  703. this
  704. )
  705. .then(res => {
  706. if (res.status === "error") failed.push(songId);
  707. else successful.push(songId);
  708. next();
  709. })
  710. .catch(err => {
  711. next(err);
  712. });
  713. },
  714. err => {
  715. if (err) next(err);
  716. else next();
  717. }
  718. );
  719. }
  720. ],
  721. async err => {
  722. if (err) {
  723. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  724. this.log("ERROR", "SONGS_REMOVE_MANY", `Failed to remove songs "${failed.join(", ")}". "${err}"`);
  725. return cb({ status: "error", message: err });
  726. }
  727. let message = "";
  728. if (successful.length === 1) message += `1 song has been successfully removed`;
  729. else message += `${successful.length} songs have been successfully removed`;
  730. if (failed.length > 0) {
  731. this.log("ERROR", "SONGS_REMOVE_MANY", `Failed to remove songs "${failed.join(", ")}". "${err}"`);
  732. if (failed.length === 1) message += `, failed to remove 1 song`;
  733. else message += `, failed to remove ${failed.length} songs`;
  734. }
  735. this.log("SUCCESS", "SONGS_REMOVE_MANY", `${message} "${successful.join(", ")}"`);
  736. return cb({
  737. status: "success",
  738. message
  739. });
  740. }
  741. );
  742. }),
  743. /**
  744. * Searches through official songs
  745. *
  746. * @param {object} session - the session object automatically added by the websocket
  747. * @param {string} query - the query
  748. * @param {string} page - the page
  749. * @param {Function} cb - gets called with the result
  750. */
  751. searchOfficial: isLoginRequired(async function searchOfficial(session, query, page, cb) {
  752. async.waterfall(
  753. [
  754. next => {
  755. if ((!query && query !== "") || typeof query !== "string") next("Invalid query.");
  756. else next();
  757. },
  758. next => {
  759. SongsModule.runJob("SEARCH", {
  760. query,
  761. includeVerified: true,
  762. trimmed: true,
  763. page
  764. })
  765. .then(response => {
  766. next(null, response);
  767. })
  768. .catch(err => {
  769. next(err);
  770. });
  771. }
  772. ],
  773. async (err, data) => {
  774. if (err) {
  775. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  776. this.log("ERROR", "SONGS_SEARCH_OFFICIAL", `Searching songs failed. "${err}"`);
  777. return cb({ status: "error", message: err });
  778. }
  779. this.log("SUCCESS", "SONGS_SEARCH_OFFICIAL", "Searching songs successful.");
  780. return cb({ status: "success", data });
  781. }
  782. );
  783. }),
  784. /**
  785. * Requests a song
  786. *
  787. * @param {object} session - the session object automatically added by the websocket
  788. * @param {string} youtubeId - the youtube id of the song that gets requested
  789. * @param {string} returnSong - returns the simple song
  790. * @param {Function} cb - gets called with the result
  791. */
  792. request: isLoginRequired(async function add(session, youtubeId, returnSong, cb) {
  793. SongsModule.runJob("REQUEST_SONG", { youtubeId, userId: session.userId }, this)
  794. .then(response => {
  795. this.log(
  796. "SUCCESS",
  797. "SONGS_REQUEST",
  798. `User "${session.userId}" successfully requested song "${youtubeId}".`
  799. );
  800. return cb({
  801. status: "success",
  802. message: "Successfully requested that song",
  803. song: returnSong ? response.song : null
  804. });
  805. })
  806. .catch(async _err => {
  807. const err = await UtilsModule.runJob("GET_ERROR", { error: _err }, this);
  808. this.log(
  809. "ERROR",
  810. "SONGS_REQUEST",
  811. `Requesting song "${youtubeId}" failed for user ${session.userId}. "${err}"`
  812. );
  813. return cb({ status: "error", message: err, song: returnSong && _err.data ? _err.data.song : null });
  814. });
  815. }),
  816. /**
  817. * Verifies a song
  818. *
  819. * @param session
  820. * @param songId - the song id
  821. * @param cb
  822. */
  823. verify: isAdminRequired(async function add(session, songId, cb) {
  824. const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  825. async.waterfall(
  826. [
  827. next => {
  828. SongModel.findOne({ _id: songId }, next);
  829. },
  830. (song, next) => {
  831. if (!song) return next("This song is not in the database.");
  832. return next(null, song);
  833. },
  834. (song, next) => {
  835. const oldStatus = false;
  836. song.verifiedBy = session.userId;
  837. song.verifiedAt = Date.now();
  838. song.verified = true;
  839. song.save(err => next(err, song, oldStatus));
  840. },
  841. (song, oldStatus, next) => {
  842. song.genres.forEach(genre => {
  843. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre, createPlaylist: true })
  844. .then(() => {})
  845. .catch(() => {});
  846. });
  847. song.artists.forEach(artist => {
  848. PlaylistsModule.runJob("AUTOFILL_ARTIST_PLAYLIST", { artist })
  849. .then(() => {})
  850. .catch(() => {});
  851. });
  852. SongsModule.runJob("UPDATE_SONG", { songId: song._id, oldStatus });
  853. next(null, song, oldStatus);
  854. }
  855. ],
  856. async err => {
  857. if (err) {
  858. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  859. this.log("ERROR", "SONGS_VERIFY", `User "${session.userId}" failed to verify song. "${err}"`);
  860. return cb({ status: "error", message: err });
  861. }
  862. this.log("SUCCESS", "SONGS_VERIFY", `User "${session.userId}" successfully verified song "${songId}".`);
  863. return cb({
  864. status: "success",
  865. message: "Song has been verified successfully."
  866. });
  867. }
  868. );
  869. // TODO Check if video is in queue and Add the song to the appropriate stations
  870. }),
  871. /**
  872. * Verify many songs
  873. *
  874. * @param session
  875. * @param songIds - array of song ids
  876. * @param cb
  877. */
  878. verifyMany: isAdminRequired(async function verifyMany(session, songIds, cb) {
  879. const successful = [];
  880. const failed = [];
  881. async.waterfall(
  882. [
  883. next => {
  884. async.eachLimit(
  885. songIds,
  886. 1,
  887. (songId, next) => {
  888. WSModule.runJob(
  889. "RUN_ACTION2",
  890. {
  891. session,
  892. namespace: "songs",
  893. action: "verify",
  894. args: [songId]
  895. },
  896. this
  897. )
  898. .then(res => {
  899. if (res.status === "error") failed.push(songId);
  900. else successful.push(songId);
  901. next();
  902. })
  903. .catch(err => {
  904. next(err);
  905. });
  906. },
  907. err => {
  908. if (err) next(err);
  909. else next();
  910. }
  911. );
  912. }
  913. ],
  914. async err => {
  915. if (err) {
  916. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  917. this.log("ERROR", "SONGS_VERIFY_MANY", `Failed to verify songs "${failed.join(", ")}". "${err}"`);
  918. return cb({ status: "error", message: err });
  919. }
  920. let message = "";
  921. if (successful.length === 1) message += `1 song has been successfully verified`;
  922. else message += `${successful.length} songs have been successfully verified`;
  923. if (failed.length > 0) {
  924. this.log("ERROR", "SONGS_VERIFY_MANY", `Failed to verify songs "${failed.join(", ")}". "${err}"`);
  925. if (failed.length === 1) message += `, failed to verify 1 song`;
  926. else message += `, failed to verify ${failed.length} songs`;
  927. }
  928. this.log("SUCCESS", "SONGS_VERIFY_MANY", `${message} "${successful.join(", ")}"`);
  929. return cb({
  930. status: "success",
  931. message
  932. });
  933. }
  934. );
  935. }),
  936. /**
  937. * Un-verifies a song
  938. *
  939. * @param session
  940. * @param songId - the song id
  941. * @param cb
  942. */
  943. unverify: isAdminRequired(async function add(session, songId, cb) {
  944. const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  945. async.waterfall(
  946. [
  947. next => {
  948. SongModel.findOne({ _id: songId }, next);
  949. },
  950. (song, next) => {
  951. if (!song) return next("This song is not in the database.");
  952. return next(null, song);
  953. },
  954. (song, next) => {
  955. song.verified = false;
  956. song.verifiedBy = null;
  957. song.verifiedAt = null;
  958. song.save(err => {
  959. next(err, song);
  960. });
  961. },
  962. (song, next) => {
  963. song.genres.forEach(genre => {
  964. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre, createPlaylist: false })
  965. .then(() => {})
  966. .catch(() => {});
  967. });
  968. song.artists.forEach(artist => {
  969. PlaylistsModule.runJob("AUTOFILL_ARTIST_PLAYLIST", { artist })
  970. .then(() => {})
  971. .catch(() => {});
  972. });
  973. SongsModule.runJob("UPDATE_SONG", { songId, oldStatus: true });
  974. next(null);
  975. }
  976. ],
  977. async err => {
  978. if (err) {
  979. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  980. this.log("ERROR", "SONGS_UNVERIFY", `User "${session.userId}" failed to verify song. "${err}"`);
  981. return cb({ status: "error", message: err });
  982. }
  983. this.log(
  984. "SUCCESS",
  985. "SONGS_UNVERIFY",
  986. `User "${session.userId}" successfully unverified song "${songId}".`
  987. );
  988. return cb({
  989. status: "success",
  990. message: "Song has been unverified successfully."
  991. });
  992. }
  993. );
  994. // TODO Check if video is in queue and Add the song to the appropriate stations
  995. }),
  996. /**
  997. * Unverify many songs
  998. *
  999. * @param session
  1000. * @param songIds - array of song ids
  1001. * @param cb
  1002. */
  1003. unverifyMany: isAdminRequired(async function unverifyMany(session, songIds, cb) {
  1004. const successful = [];
  1005. const failed = [];
  1006. async.waterfall(
  1007. [
  1008. next => {
  1009. async.eachLimit(
  1010. songIds,
  1011. 1,
  1012. (songId, next) => {
  1013. WSModule.runJob(
  1014. "RUN_ACTION2",
  1015. {
  1016. session,
  1017. namespace: "songs",
  1018. action: "unverify",
  1019. args: [songId]
  1020. },
  1021. this
  1022. )
  1023. .then(res => {
  1024. if (res.status === "error") failed.push(songId);
  1025. else successful.push(songId);
  1026. next();
  1027. })
  1028. .catch(err => {
  1029. next(err);
  1030. });
  1031. },
  1032. err => {
  1033. if (err) next(err);
  1034. else next();
  1035. }
  1036. );
  1037. }
  1038. ],
  1039. async err => {
  1040. if (err) {
  1041. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1042. this.log(
  1043. "ERROR",
  1044. "SONGS_UNVERIFY_MANY",
  1045. `Failed to unverify songs "${failed.join(", ")}". "${err}"`
  1046. );
  1047. return cb({ status: "error", message: err });
  1048. }
  1049. let message = "";
  1050. if (successful.length === 1) message += `1 song has been successfully unverified`;
  1051. else message += `${successful.length} songs have been successfully unverified`;
  1052. if (failed.length > 0) {
  1053. this.log(
  1054. "ERROR",
  1055. "SONGS_UNVERIFY_MANY",
  1056. `Failed to unverify songs "${failed.join(", ")}". "${err}"`
  1057. );
  1058. if (failed.length === 1) message += `, failed to unverify 1 song`;
  1059. else message += `, failed to unverify ${failed.length} songs`;
  1060. }
  1061. this.log("SUCCESS", "SONGS_UNVERIFY_MANY", `${message} "${successful.join(", ")}"`);
  1062. return cb({
  1063. status: "success",
  1064. message
  1065. });
  1066. }
  1067. );
  1068. }),
  1069. /**
  1070. * Requests a set of songs
  1071. *
  1072. * @param {object} session - the session object automatically added by the websocket
  1073. * @param {string} url - the url of the the YouTube playlist
  1074. * @param {boolean} musicOnly - whether to only get music from the playlist
  1075. * @param {Function} cb - gets called with the result
  1076. */
  1077. requestSet: isLoginRequired(function requestSet(session, url, musicOnly, returnSongs, cb) {
  1078. async.waterfall(
  1079. [
  1080. next => {
  1081. YouTubeModule.runJob(
  1082. "GET_PLAYLIST",
  1083. {
  1084. url,
  1085. musicOnly
  1086. },
  1087. this
  1088. )
  1089. .then(res => {
  1090. next(null, res.songs);
  1091. })
  1092. .catch(next);
  1093. },
  1094. (youtubeIds, next) => {
  1095. let successful = 0;
  1096. let songs = {};
  1097. let failed = 0;
  1098. let alreadyInDatabase = 0;
  1099. if (youtubeIds.length === 0) next();
  1100. async.eachOfLimit(
  1101. youtubeIds,
  1102. 1,
  1103. (youtubeId, index, next) => {
  1104. WSModule.runJob(
  1105. "RUN_ACTION2",
  1106. {
  1107. session,
  1108. namespace: "songs",
  1109. action: "request",
  1110. args: [youtubeId, returnSongs]
  1111. },
  1112. this
  1113. )
  1114. .then(res => {
  1115. if (res.status === "success") successful += 1;
  1116. else failed += 1;
  1117. if (res.message === "This song is already in the database.") alreadyInDatabase += 1;
  1118. if (res.song) songs[index] = res.song;
  1119. })
  1120. .catch(() => {
  1121. failed += 1;
  1122. })
  1123. .finally(() => {
  1124. next();
  1125. });
  1126. },
  1127. () => {
  1128. if (returnSongs)
  1129. songs = Object.keys(songs)
  1130. .sort()
  1131. .map(key => songs[key]);
  1132. next(null, { successful, failed, alreadyInDatabase, songs });
  1133. }
  1134. );
  1135. }
  1136. ],
  1137. async (err, response) => {
  1138. if (err) {
  1139. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1140. this.log(
  1141. "ERROR",
  1142. "REQUEST_SET",
  1143. `Importing a YouTube playlist to be requested failed for user "${session.userId}". "${err}"`
  1144. );
  1145. return cb({ status: "error", message: err });
  1146. }
  1147. this.log(
  1148. "SUCCESS",
  1149. "REQUEST_SET",
  1150. `Successfully imported a YouTube playlist to be requested for user "${session.userId}".`
  1151. );
  1152. return cb({
  1153. status: "success",
  1154. message: `Playlist is done importing. ${response.successful} were added succesfully, ${response.failed} failed (${response.alreadyInDatabase} were already in database)`,
  1155. songs: returnSongs ? response.songs : null
  1156. });
  1157. }
  1158. );
  1159. }),
  1160. /**
  1161. * Likes a song
  1162. *
  1163. * @param session
  1164. * @param youtubeId - the youtube id
  1165. * @param cb
  1166. */
  1167. like: isLoginRequired(async function like(session, youtubeId, cb) {
  1168. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1169. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1170. async.waterfall(
  1171. [
  1172. next => songModel.findOne({ youtubeId }, next),
  1173. (song, next) => {
  1174. if (!song) return next("No song found with that id.");
  1175. return next(null, song);
  1176. },
  1177. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  1178. (song, user, next) => {
  1179. if (!user) return next("User does not exist.");
  1180. return this.module
  1181. .runJob(
  1182. "RUN_ACTION2",
  1183. {
  1184. session,
  1185. namespace: "playlists",
  1186. action: "removeSongFromPlaylist",
  1187. args: [youtubeId, user.dislikedSongsPlaylist]
  1188. },
  1189. this
  1190. )
  1191. .then(res => {
  1192. if (res.status === "error" && res.message !== "Song wasn't in playlist.")
  1193. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  1194. return next(null, song, user.likedSongsPlaylist);
  1195. })
  1196. .catch(err => next(err));
  1197. },
  1198. (song, likedSongsPlaylist, next) =>
  1199. this.module
  1200. .runJob(
  1201. "RUN_ACTION2",
  1202. {
  1203. session,
  1204. namespace: "playlists",
  1205. action: "addSongToPlaylist",
  1206. args: [false, youtubeId, likedSongsPlaylist]
  1207. },
  1208. this
  1209. )
  1210. .then(res => {
  1211. if (res.status === "error") {
  1212. if (res.message === "That song is already in the playlist")
  1213. return next("You have already liked this song.");
  1214. return next("Unable to add song to the 'Liked Songs' playlist.");
  1215. }
  1216. return next(null, song);
  1217. })
  1218. .catch(err => next(err)),
  1219. (song, next) => {
  1220. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1221. .then(ratings => next(null, song, ratings))
  1222. .catch(err => next(err));
  1223. }
  1224. ],
  1225. async (err, song, ratings) => {
  1226. if (err) {
  1227. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1228. this.log(
  1229. "ERROR",
  1230. "SONGS_LIKE",
  1231. `User "${session.userId}" failed to like song ${youtubeId}. "${err}"`
  1232. );
  1233. return cb({ status: "error", message: err });
  1234. }
  1235. const { likes, dislikes } = ratings;
  1236. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1237. CacheModule.runJob("PUB", {
  1238. channel: "song.like",
  1239. value: JSON.stringify({
  1240. youtubeId,
  1241. userId: session.userId,
  1242. likes,
  1243. dislikes
  1244. })
  1245. });
  1246. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1247. userId: session.userId,
  1248. type: "song__like",
  1249. payload: {
  1250. message: `Liked song <youtubeId>${song.title} by ${song.artists.join(", ")}</youtubeId>`,
  1251. youtubeId,
  1252. thumbnail: song.thumbnail
  1253. }
  1254. });
  1255. return cb({
  1256. status: "success",
  1257. message: "You have successfully liked this song."
  1258. });
  1259. }
  1260. );
  1261. }),
  1262. /**
  1263. * Dislikes a song
  1264. *
  1265. * @param session
  1266. * @param youtubeId - the youtube id
  1267. * @param cb
  1268. */
  1269. dislike: isLoginRequired(async function dislike(session, youtubeId, cb) {
  1270. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1271. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1272. async.waterfall(
  1273. [
  1274. next => {
  1275. songModel.findOne({ youtubeId }, next);
  1276. },
  1277. (song, next) => {
  1278. if (!song) return next("No song found with that id.");
  1279. return next(null, song);
  1280. },
  1281. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  1282. (song, user, next) => {
  1283. if (!user) return next("User does not exist.");
  1284. return this.module
  1285. .runJob(
  1286. "RUN_ACTION2",
  1287. {
  1288. session,
  1289. namespace: "playlists",
  1290. action: "removeSongFromPlaylist",
  1291. args: [youtubeId, user.likedSongsPlaylist]
  1292. },
  1293. this
  1294. )
  1295. .then(res => {
  1296. if (res.status === "error" && res.message !== "Song wasn't in playlist.")
  1297. return next("Unable to remove song from the 'Liked Songs' playlist.");
  1298. return next(null, song, user.dislikedSongsPlaylist);
  1299. })
  1300. .catch(err => next(err));
  1301. },
  1302. (song, dislikedSongsPlaylist, next) =>
  1303. this.module
  1304. .runJob(
  1305. "RUN_ACTION2",
  1306. {
  1307. session,
  1308. namespace: "playlists",
  1309. action: "addSongToPlaylist",
  1310. args: [false, youtubeId, dislikedSongsPlaylist]
  1311. },
  1312. this
  1313. )
  1314. .then(res => {
  1315. if (res.status === "error") {
  1316. if (res.message === "That song is already in the playlist")
  1317. return next("You have already disliked this song.");
  1318. return next("Unable to add song to the 'Disliked Songs' playlist.");
  1319. }
  1320. return next(null, song);
  1321. })
  1322. .catch(err => next(err)),
  1323. (song, next) => {
  1324. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1325. .then(ratings => next(null, song, ratings))
  1326. .catch(err => next(err));
  1327. }
  1328. ],
  1329. async (err, song, ratings) => {
  1330. if (err) {
  1331. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1332. this.log(
  1333. "ERROR",
  1334. "SONGS_DISLIKE",
  1335. `User "${session.userId}" failed to dislike song ${youtubeId}. "${err}"`
  1336. );
  1337. return cb({ status: "error", message: err });
  1338. }
  1339. const { likes, dislikes } = ratings;
  1340. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1341. CacheModule.runJob("PUB", {
  1342. channel: "song.dislike",
  1343. value: JSON.stringify({
  1344. youtubeId,
  1345. userId: session.userId,
  1346. likes,
  1347. dislikes
  1348. })
  1349. });
  1350. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1351. userId: session.userId,
  1352. type: "song__dislike",
  1353. payload: {
  1354. message: `Disliked song <youtubeId>${song.title} by ${song.artists.join(", ")}</youtubeId>`,
  1355. youtubeId,
  1356. thumbnail: song.thumbnail
  1357. }
  1358. });
  1359. return cb({
  1360. status: "success",
  1361. message: "You have successfully disliked this song."
  1362. });
  1363. }
  1364. );
  1365. }),
  1366. /**
  1367. * Undislikes a song
  1368. *
  1369. * @param session
  1370. * @param youtubeId - the youtube id
  1371. * @param cb
  1372. */
  1373. undislike: isLoginRequired(async function undislike(session, youtubeId, cb) {
  1374. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1375. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1376. async.waterfall(
  1377. [
  1378. next => {
  1379. songModel.findOne({ youtubeId }, next);
  1380. },
  1381. (song, next) => {
  1382. if (!song) return next("No song found with that id.");
  1383. return next(null, song);
  1384. },
  1385. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  1386. (song, user, next) => {
  1387. if (!user) return next("User does not exist.");
  1388. return this.module
  1389. .runJob(
  1390. "RUN_ACTION2",
  1391. {
  1392. session,
  1393. namespace: "playlists",
  1394. action: "removeSongFromPlaylist",
  1395. args: [youtubeId, user.dislikedSongsPlaylist]
  1396. },
  1397. this
  1398. )
  1399. .then(res => {
  1400. if (res.status === "error")
  1401. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  1402. return next(null, song, user.likedSongsPlaylist);
  1403. })
  1404. .catch(err => next(err));
  1405. },
  1406. (song, likedSongsPlaylist, next) => {
  1407. this.module
  1408. .runJob(
  1409. "RUN_ACTION2",
  1410. {
  1411. session,
  1412. namespace: "playlists",
  1413. action: "removeSongFromPlaylist",
  1414. args: [youtubeId, likedSongsPlaylist]
  1415. },
  1416. this
  1417. )
  1418. .then(res => {
  1419. if (res.status === "error" && res.message !== "Song wasn't in playlist.")
  1420. return next("Unable to remove song from the 'Liked Songs' playlist.");
  1421. return next(null, song);
  1422. })
  1423. .catch(err => next(err));
  1424. },
  1425. (song, next) => {
  1426. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1427. .then(ratings => next(null, song, ratings))
  1428. .catch(err => next(err));
  1429. }
  1430. ],
  1431. async (err, song, ratings) => {
  1432. if (err) {
  1433. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1434. this.log(
  1435. "ERROR",
  1436. "SONGS_UNDISLIKE",
  1437. `User "${session.userId}" failed to undislike song ${youtubeId}. "${err}"`
  1438. );
  1439. return cb({ status: "error", message: err });
  1440. }
  1441. const { likes, dislikes } = ratings;
  1442. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1443. CacheModule.runJob("PUB", {
  1444. channel: "song.undislike",
  1445. value: JSON.stringify({
  1446. youtubeId,
  1447. userId: session.userId,
  1448. likes,
  1449. dislikes
  1450. })
  1451. });
  1452. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1453. userId: session.userId,
  1454. type: "song__undislike",
  1455. payload: {
  1456. message: `Removed <youtubeId>${song.title} by ${song.artists.join(
  1457. ", "
  1458. )}</youtubeId> from your Disliked Songs`,
  1459. youtubeId,
  1460. thumbnail: song.thumbnail
  1461. }
  1462. });
  1463. return cb({
  1464. status: "success",
  1465. message: "You have successfully undisliked this song."
  1466. });
  1467. }
  1468. );
  1469. }),
  1470. /**
  1471. * Unlikes a song
  1472. *
  1473. * @param session
  1474. * @param youtubeId - the youtube id
  1475. * @param cb
  1476. */
  1477. unlike: isLoginRequired(async function unlike(session, youtubeId, cb) {
  1478. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1479. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1480. async.waterfall(
  1481. [
  1482. next => {
  1483. songModel.findOne({ youtubeId }, next);
  1484. },
  1485. (song, next) => {
  1486. if (!song) return next("No song found with that id.");
  1487. return next(null, song);
  1488. },
  1489. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  1490. (song, user, next) => {
  1491. if (!user) return next("User does not exist.");
  1492. return this.module
  1493. .runJob(
  1494. "RUN_ACTION2",
  1495. {
  1496. session,
  1497. namespace: "playlists",
  1498. action: "removeSongFromPlaylist",
  1499. args: [youtubeId, user.dislikedSongsPlaylist]
  1500. },
  1501. this
  1502. )
  1503. .then(res => {
  1504. if (res.status === "error" && res.message !== "Song wasn't in playlist.")
  1505. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  1506. return next(null, song, user.likedSongsPlaylist);
  1507. })
  1508. .catch(err => next(err));
  1509. },
  1510. (song, likedSongsPlaylist, next) => {
  1511. this.module
  1512. .runJob(
  1513. "RUN_ACTION2",
  1514. {
  1515. session,
  1516. namespace: "playlists",
  1517. action: "removeSongFromPlaylist",
  1518. args: [youtubeId, likedSongsPlaylist]
  1519. },
  1520. this
  1521. )
  1522. .then(res => {
  1523. if (res.status === "error")
  1524. return next("Unable to remove song from the 'Liked Songs' playlist.");
  1525. return next(null, song);
  1526. })
  1527. .catch(err => next(err));
  1528. },
  1529. (song, next) => {
  1530. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1531. .then(ratings => next(null, song, ratings))
  1532. .catch(err => next(err));
  1533. }
  1534. ],
  1535. async (err, song, ratings) => {
  1536. if (err) {
  1537. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1538. this.log(
  1539. "ERROR",
  1540. "SONGS_UNLIKE",
  1541. `User "${session.userId}" failed to unlike song ${youtubeId}. "${err}"`
  1542. );
  1543. return cb({ status: "error", message: err });
  1544. }
  1545. const { likes, dislikes } = ratings;
  1546. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1547. CacheModule.runJob("PUB", {
  1548. channel: "song.unlike",
  1549. value: JSON.stringify({
  1550. youtubeId,
  1551. userId: session.userId,
  1552. likes,
  1553. dislikes
  1554. })
  1555. });
  1556. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1557. userId: session.userId,
  1558. type: "song__unlike",
  1559. payload: {
  1560. message: `Removed <youtubeId>${song.title} by ${song.artists.join(
  1561. ", "
  1562. )}</youtubeId> from your Liked Songs`,
  1563. youtubeId,
  1564. thumbnail: song.thumbnail
  1565. }
  1566. });
  1567. return cb({
  1568. status: "success",
  1569. message: "You have successfully unliked this song."
  1570. });
  1571. }
  1572. );
  1573. }),
  1574. /**
  1575. * Gets song ratings
  1576. *
  1577. * @param session
  1578. * @param songId - the Musare song id
  1579. * @param cb
  1580. */
  1581. getSongRatings: isLoginRequired(async function getSongRatings(session, songId, cb) {
  1582. async.waterfall(
  1583. [
  1584. next => {
  1585. SongsModule.runJob("GET_SONG", { songId }, this)
  1586. .then(res => next(null, res.song))
  1587. .catch(next);
  1588. },
  1589. (song, next) => {
  1590. next(null, {
  1591. likes: song.likes,
  1592. dislikes: song.dislikes
  1593. });
  1594. }
  1595. ],
  1596. async (err, ratings) => {
  1597. if (err) {
  1598. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1599. this.log(
  1600. "ERROR",
  1601. "SONGS_GET_RATINGS",
  1602. `User "${session.userId}" failed to get ratings for ${songId}. "${err}"`
  1603. );
  1604. return cb({ status: "error", message: err });
  1605. }
  1606. const { likes, dislikes } = ratings;
  1607. return cb({
  1608. status: "success",
  1609. data: {
  1610. likes,
  1611. dislikes
  1612. }
  1613. });
  1614. }
  1615. );
  1616. }),
  1617. /**
  1618. * Gets user's own song ratings
  1619. *
  1620. * @param session
  1621. * @param youtubeId - the youtube id
  1622. * @param cb
  1623. */
  1624. getOwnSongRatings: isLoginRequired(async function getOwnSongRatings(session, youtubeId, cb) {
  1625. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  1626. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1627. async.waterfall(
  1628. [
  1629. next => songModel.findOne({ youtubeId }, next),
  1630. (song, next) => {
  1631. if (!song) return next("No song found with that id.");
  1632. return next(null);
  1633. },
  1634. next =>
  1635. playlistModel.findOne(
  1636. { createdBy: session.userId, displayName: "Liked Songs" },
  1637. (err, playlist) => {
  1638. if (err) return next(err);
  1639. if (!playlist) return next("'Liked Songs' playlist does not exist.");
  1640. let isLiked = false;
  1641. Object.values(playlist.songs).forEach(song => {
  1642. // song is found in 'liked songs' playlist
  1643. if (song.youtubeId === youtubeId) isLiked = true;
  1644. });
  1645. return next(null, isLiked);
  1646. }
  1647. ),
  1648. (isLiked, next) =>
  1649. playlistModel.findOne(
  1650. { createdBy: session.userId, displayName: "Disliked Songs" },
  1651. (err, playlist) => {
  1652. if (err) return next(err);
  1653. if (!playlist) return next("'Disliked Songs' playlist does not exist.");
  1654. const ratings = { isLiked, isDisliked: false };
  1655. Object.values(playlist.songs).forEach(song => {
  1656. // song is found in 'disliked songs' playlist
  1657. if (song.youtubeId === youtubeId) ratings.isDisliked = true;
  1658. });
  1659. return next(null, ratings);
  1660. }
  1661. )
  1662. ],
  1663. async (err, ratings) => {
  1664. if (err) {
  1665. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1666. this.log(
  1667. "ERROR",
  1668. "SONGS_GET_OWN_RATINGS",
  1669. `User "${session.userId}" failed to get ratings for ${youtubeId}. "${err}"`
  1670. );
  1671. return cb({ status: "error", message: err });
  1672. }
  1673. const { isLiked, isDisliked } = ratings;
  1674. return cb({
  1675. status: "success",
  1676. data: {
  1677. youtubeId,
  1678. liked: isLiked,
  1679. disliked: isDisliked
  1680. }
  1681. });
  1682. }
  1683. );
  1684. }),
  1685. /**
  1686. * Gets a list of all genres
  1687. *
  1688. * @param session
  1689. * @param cb
  1690. */
  1691. getGenres: isAdminRequired(function getGenres(session, cb) {
  1692. async.waterfall(
  1693. [
  1694. next => {
  1695. SongsModule.runJob("GET_GENRES", this)
  1696. .then(res => {
  1697. next(null, res.genres);
  1698. })
  1699. .catch(next);
  1700. }
  1701. ],
  1702. async (err, genres) => {
  1703. if (err && err !== true) {
  1704. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1705. this.log("ERROR", "GET_GENRES", `User ${session.userId} failed to get genres. '${err}'`);
  1706. cb({ status: "error", message: err });
  1707. } else {
  1708. this.log("SUCCESS", "GET_GENRES", `User ${session.userId} has successfully got the genres.`);
  1709. cb({
  1710. status: "success",
  1711. message: "Successfully got genres.",
  1712. data: {
  1713. items: genres
  1714. }
  1715. });
  1716. }
  1717. }
  1718. );
  1719. }),
  1720. /**
  1721. * Bulk update genres for selected songs
  1722. *
  1723. * @param session
  1724. * @param method Whether to add, remove or replace genres
  1725. * @param genres Array of genres to apply
  1726. * @param songIds Array of songIds to apply genres to
  1727. * @param cb
  1728. */
  1729. editGenres: isAdminRequired(async function editGenres(session, method, genres, songIds, cb) {
  1730. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1731. async.waterfall(
  1732. [
  1733. next => {
  1734. songModel.find({ _id: { $in: songIds } }, next);
  1735. },
  1736. (songs, next) => {
  1737. const songsFound = songs.map(song => song._id);
  1738. if (songsFound.length > 0) next(null, songsFound);
  1739. else next("None of the specified songs were found.");
  1740. },
  1741. (songsFound, next) => {
  1742. const query = {};
  1743. if (method === "add") {
  1744. query.$addToSet = { genres: { $each: genres } };
  1745. } else if (method === "remove") {
  1746. query.$pullAll = { genres };
  1747. } else if (method === "replace") {
  1748. query.$set = { genres };
  1749. } else {
  1750. next("Invalid method.");
  1751. return;
  1752. }
  1753. songModel.updateMany({ _id: { $in: songsFound } }, query, { runValidators: true }, err => {
  1754. if (err) {
  1755. next(err);
  1756. return;
  1757. }
  1758. SongsModule.runJob("UPDATE_SONGS", { songIds: songsFound });
  1759. next();
  1760. });
  1761. }
  1762. ],
  1763. async err => {
  1764. if (err && err !== true) {
  1765. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1766. this.log("ERROR", "EDIT_GENRES", `User ${session.userId} failed to edit genres. '${err}'`);
  1767. cb({ status: "error", message: err });
  1768. } else {
  1769. this.log("SUCCESS", "EDIT_GENRES", `User ${session.userId} has successfully edited genres.`);
  1770. cb({
  1771. status: "success",
  1772. message: "Successfully edited genres."
  1773. });
  1774. }
  1775. }
  1776. );
  1777. }),
  1778. /**
  1779. * Gets a list of all artists
  1780. *
  1781. * @param session
  1782. * @param cb
  1783. */
  1784. getArtists: isAdminRequired(function getArtists(session, cb) {
  1785. async.waterfall(
  1786. [
  1787. next => {
  1788. SongsModule.runJob("GET_ARTISTS", this)
  1789. .then(res => {
  1790. next(null, res.artists);
  1791. })
  1792. .catch(next);
  1793. }
  1794. ],
  1795. async (err, artists) => {
  1796. if (err && err !== true) {
  1797. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1798. this.log("ERROR", "GET_ARTISTS", `User ${session.userId} failed to get artists. '${err}'`);
  1799. cb({ status: "error", message: err });
  1800. } else {
  1801. this.log("SUCCESS", "GET_ARTISTS", `User ${session.userId} has successfully got the artists.`);
  1802. cb({
  1803. status: "success",
  1804. message: "Successfully got artists.",
  1805. data: {
  1806. items: artists
  1807. }
  1808. });
  1809. }
  1810. }
  1811. );
  1812. }),
  1813. /**
  1814. * Bulk update artists for selected songs
  1815. *
  1816. * @param session
  1817. * @param method Whether to add, remove or replace artists
  1818. * @param artists Array of artists to apply
  1819. * @param songIds Array of songIds to apply artists to
  1820. * @param cb
  1821. */
  1822. editArtists: isAdminRequired(async function editArtists(session, method, artists, songIds, cb) {
  1823. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1824. async.waterfall(
  1825. [
  1826. next => {
  1827. songModel.find({ _id: { $in: songIds } }, next);
  1828. },
  1829. (songs, next) => {
  1830. const songsFound = songs.map(song => song._id);
  1831. if (songsFound.length > 0) next(null, songsFound);
  1832. else next("None of the specified songs were found.");
  1833. },
  1834. (songsFound, next) => {
  1835. const query = {};
  1836. if (method === "add") {
  1837. query.$addToSet = { artists: { $each: artists } };
  1838. } else if (method === "remove") {
  1839. query.$pullAll = { artists };
  1840. } else if (method === "replace") {
  1841. query.$set = { artists };
  1842. } else {
  1843. next("Invalid method.");
  1844. return;
  1845. }
  1846. songModel.updateMany({ _id: { $in: songsFound } }, query, { runValidators: true }, err => {
  1847. if (err) {
  1848. next(err);
  1849. return;
  1850. }
  1851. SongsModule.runJob("UPDATE_SONGS", { songIds: songsFound });
  1852. next();
  1853. });
  1854. }
  1855. ],
  1856. async err => {
  1857. if (err && err !== true) {
  1858. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1859. this.log("ERROR", "EDIT_ARTISTS", `User ${session.userId} failed to edit artists. '${err}'`);
  1860. cb({ status: "error", message: err });
  1861. } else {
  1862. this.log("SUCCESS", "EDIT_ARTISTS", `User ${session.userId} has successfully edited artists.`);
  1863. cb({
  1864. status: "success",
  1865. message: "Successfully edited artists."
  1866. });
  1867. }
  1868. }
  1869. );
  1870. }),
  1871. /**
  1872. * Gets a list of all tags
  1873. *
  1874. * @param session
  1875. * @param cb
  1876. */
  1877. getTags: isAdminRequired(function getTags(session, cb) {
  1878. async.waterfall(
  1879. [
  1880. next => {
  1881. SongsModule.runJob("GET_TAGS", this)
  1882. .then(res => {
  1883. next(null, res.tags);
  1884. })
  1885. .catch(next);
  1886. }
  1887. ],
  1888. async (err, tags) => {
  1889. if (err && err !== true) {
  1890. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1891. this.log("ERROR", "GET_TAGS", `User ${session.userId} failed to get tags. '${err}'`);
  1892. cb({ status: "error", message: err });
  1893. } else {
  1894. this.log("SUCCESS", "GET_TAGS", `User ${session.userId} has successfully got the tags.`);
  1895. cb({
  1896. status: "success",
  1897. message: "Successfully got tags.",
  1898. data: {
  1899. items: tags
  1900. }
  1901. });
  1902. }
  1903. }
  1904. );
  1905. }),
  1906. /**
  1907. * Bulk update tags for selected songs
  1908. *
  1909. * @param session
  1910. * @param method Whether to add, remove or replace tags
  1911. * @param tags Array of tags to apply
  1912. * @param songIds Array of songIds to apply tags to
  1913. * @param cb
  1914. */
  1915. editTags: isAdminRequired(async function editTags(session, method, tags, songIds, cb) {
  1916. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1917. async.waterfall(
  1918. [
  1919. next => {
  1920. songModel.find({ _id: { $in: songIds } }, next);
  1921. },
  1922. (songs, next) => {
  1923. const songsFound = songs.map(song => song._id);
  1924. if (songsFound.length > 0) next(null, songsFound);
  1925. else next("None of the specified songs were found.");
  1926. },
  1927. (songsFound, next) => {
  1928. const query = {};
  1929. if (method === "add") {
  1930. query.$addToSet = { tags: { $each: tags } };
  1931. } else if (method === "remove") {
  1932. query.$pullAll = { tags };
  1933. } else if (method === "replace") {
  1934. query.$set = { tags };
  1935. } else {
  1936. next("Invalid method.");
  1937. return;
  1938. }
  1939. songModel.updateMany({ _id: { $in: songsFound } }, query, { runValidators: true }, err => {
  1940. if (err) {
  1941. next(err);
  1942. return;
  1943. }
  1944. SongsModule.runJob("UPDATE_SONGS", { songIds: songsFound });
  1945. next();
  1946. });
  1947. }
  1948. ],
  1949. async err => {
  1950. if (err && err !== true) {
  1951. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1952. this.log("ERROR", "EDIT_TAGS", `User ${session.userId} failed to edit tags. '${err}'`);
  1953. cb({ status: "error", message: err });
  1954. } else {
  1955. this.log("SUCCESS", "EDIT_TAGS", `User ${session.userId} has successfully edited tags.`);
  1956. cb({
  1957. status: "success",
  1958. message: "Successfully edited tags."
  1959. });
  1960. }
  1961. }
  1962. );
  1963. })
  1964. };