songs.js 53 KB

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