songs.js 53 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109
  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. // Verify the song
  473. if (existingSong.verified === false && song.verified === true) {
  474. song.verifiedBy = session.userId;
  475. song.verifiedAt = Date.now();
  476. }
  477. // Unverify the song
  478. else if (existingSong.verified === true && song.verified === false) {
  479. song.verifiedBy = null;
  480. song.verifiedAt = null;
  481. }
  482. next();
  483. },
  484. next => {
  485. songModel.updateOne({ _id: songId }, song, { runValidators: true }, next);
  486. },
  487. (res, next) => {
  488. SongsModule.runJob("UPDATE_SONG", { songId }, this)
  489. .then(song => {
  490. existingSong.genres
  491. .concat(song.genres)
  492. .filter((value, index, self) => self.indexOf(value) === index)
  493. .forEach(genre => {
  494. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
  495. .then(() => {})
  496. .catch(() => {});
  497. });
  498. next(null, song);
  499. })
  500. .catch(next);
  501. }
  502. ],
  503. async (err, song) => {
  504. if (err) {
  505. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  506. this.log("ERROR", "SONGS_UPDATE", `Failed to update song "${songId}". "${err}"`);
  507. return cb({ status: "error", message: err });
  508. }
  509. this.log("SUCCESS", "SONGS_UPDATE", `Successfully updated song "${songId}".`);
  510. return cb({
  511. status: "success",
  512. message: "Song has been successfully updated",
  513. data: { song }
  514. });
  515. }
  516. );
  517. }),
  518. /**
  519. * Removes a song
  520. *
  521. * @param session
  522. * @param songId - the song id
  523. * @param cb
  524. */
  525. remove: isAdminRequired(async function remove(session, songId, cb) {
  526. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  527. const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
  528. async.waterfall(
  529. [
  530. next => {
  531. songModel.findOne({ _id: songId }, next);
  532. },
  533. (song, next) => {
  534. PlaylistsModule.runJob("GET_PLAYLISTS_WITH_SONG", { songId }, this)
  535. .then(res => {
  536. async.eachLimit(
  537. res.playlists,
  538. 1,
  539. (playlist, next) => {
  540. WSModule.runJob(
  541. "RUN_ACTION2",
  542. {
  543. session,
  544. namespace: "playlists",
  545. action: "removeSongFromPlaylist",
  546. args: [song.youtubeId, playlist._id]
  547. },
  548. this
  549. )
  550. .then(res => {
  551. if (res.status === "error") next(res.message);
  552. else next();
  553. })
  554. .catch(err => {
  555. next(err);
  556. });
  557. },
  558. err => {
  559. if (err) next(err);
  560. else next(null, song);
  561. }
  562. );
  563. })
  564. .catch(err => next(err));
  565. },
  566. (song, next) => {
  567. stationModel.find({ "queue._id": songId }, (err, stations) => {
  568. if (err) next(err);
  569. else {
  570. async.eachLimit(
  571. stations,
  572. 1,
  573. (station, next) => {
  574. WSModule.runJob(
  575. "RUN_ACTION2",
  576. {
  577. session,
  578. namespace: "stations",
  579. action: "removeFromQueue",
  580. args: [station._id, song.youtubeId]
  581. },
  582. this
  583. )
  584. .then(res => {
  585. if (
  586. res.status === "error" &&
  587. res.message !== "Station not found" &&
  588. res.message !== "Song is not currently in the queue."
  589. )
  590. next(res.message);
  591. else next();
  592. })
  593. .catch(err => {
  594. next(err);
  595. });
  596. },
  597. err => {
  598. if (err) next(err);
  599. else next();
  600. }
  601. );
  602. }
  603. });
  604. },
  605. next => {
  606. stationModel.find({ "currentSong._id": songId }, (err, stations) => {
  607. if (err) next(err);
  608. else {
  609. async.eachLimit(
  610. stations,
  611. 1,
  612. (station, next) => {
  613. StationsModule.runJob(
  614. "SKIP_STATION",
  615. { stationId: station._id, natural: false },
  616. this
  617. )
  618. .then(() => {
  619. next();
  620. })
  621. .catch(err => {
  622. if (err.message === "Station not found.") next();
  623. else next(err);
  624. });
  625. },
  626. err => {
  627. if (err) next(err);
  628. else next();
  629. }
  630. );
  631. }
  632. });
  633. },
  634. next => {
  635. songModel.deleteOne({ _id: songId }, err => {
  636. if (err) next(err);
  637. else next();
  638. });
  639. },
  640. next => {
  641. CacheModule.runJob("HDEL", { table: "songs", key: songId }, this)
  642. .then(() => {
  643. next();
  644. })
  645. .catch(next);
  646. }
  647. ],
  648. async err => {
  649. if (err) {
  650. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  651. this.log("ERROR", "SONGS_REMOVE", `Failed to remove song "${songId}". "${err}"`);
  652. return cb({ status: "error", message: err });
  653. }
  654. this.log("SUCCESS", "SONGS_REMOVE", `Successfully removed song "${songId}".`);
  655. CacheModule.runJob("PUB", {
  656. channel: "song.removed",
  657. value: { songId }
  658. });
  659. return cb({
  660. status: "success",
  661. message: "Song has been successfully removed"
  662. });
  663. }
  664. );
  665. }),
  666. /**
  667. * Removes many songs
  668. *
  669. * @param session
  670. * @param songIds - array of song ids
  671. * @param cb
  672. */
  673. removeMany: isAdminRequired(async function remove(session, songIds, cb) {
  674. const successful = [];
  675. const failed = [];
  676. async.waterfall(
  677. [
  678. next => {
  679. async.eachLimit(
  680. songIds,
  681. 1,
  682. (songId, next) => {
  683. WSModule.runJob(
  684. "RUN_ACTION2",
  685. {
  686. session,
  687. namespace: "songs",
  688. action: "remove",
  689. args: [songId]
  690. },
  691. this
  692. )
  693. .then(res => {
  694. if (res.status === "error") failed.push(songId);
  695. else successful.push(songId);
  696. next();
  697. })
  698. .catch(err => {
  699. next(err);
  700. });
  701. },
  702. err => {
  703. if (err) next(err);
  704. else next();
  705. }
  706. );
  707. }
  708. ],
  709. async err => {
  710. if (err) {
  711. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  712. this.log("ERROR", "SONGS_REMOVE_MANY", `Failed to remove songs "${failed.join(", ")}". "${err}"`);
  713. return cb({ status: "error", message: err });
  714. }
  715. let message = "";
  716. if (successful.length === 1) message += `1 song has been successfully removed`;
  717. else message += `${successful.length} songs have been successfully removed`;
  718. if (failed.length > 0) {
  719. this.log("ERROR", "SONGS_REMOVE_MANY", `Failed to remove songs "${failed.join(", ")}". "${err}"`);
  720. if (failed.length === 1) message += `, failed to remove 1 song`;
  721. else message += `, failed to remove ${failed.length} songs`;
  722. }
  723. this.log("SUCCESS", "SONGS_REMOVE_MANY", `${message} "${successful.join(", ")}"`);
  724. return cb({
  725. status: "success",
  726. message
  727. });
  728. }
  729. );
  730. }),
  731. /**
  732. * Searches through official songs
  733. *
  734. * @param {object} session - the session object automatically added by the websocket
  735. * @param {string} query - the query
  736. * @param {string} page - the page
  737. * @param {Function} cb - gets called with the result
  738. */
  739. searchOfficial: isLoginRequired(async function searchOfficial(session, query, page, cb) {
  740. async.waterfall(
  741. [
  742. next => {
  743. if ((!query && query !== "") || typeof query !== "string") next("Invalid query.");
  744. else next();
  745. },
  746. next => {
  747. SongsModule.runJob("SEARCH", {
  748. query,
  749. includeVerified: true,
  750. trimmed: true,
  751. page
  752. })
  753. .then(response => {
  754. next(null, response);
  755. })
  756. .catch(err => {
  757. next(err);
  758. });
  759. }
  760. ],
  761. async (err, data) => {
  762. if (err) {
  763. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  764. this.log("ERROR", "SONGS_SEARCH_OFFICIAL", `Searching songs failed. "${err}"`);
  765. return cb({ status: "error", message: err });
  766. }
  767. this.log("SUCCESS", "SONGS_SEARCH_OFFICIAL", "Searching songs successful.");
  768. return cb({ status: "success", data });
  769. }
  770. );
  771. }),
  772. /**
  773. * Requests a song
  774. *
  775. * @param {object} session - the session object automatically added by the websocket
  776. * @param {string} youtubeId - the youtube id of the song that gets requested
  777. * @param {string} returnSong - returns the simple song
  778. * @param {Function} cb - gets called with the result
  779. */
  780. request: isLoginRequired(async function add(session, youtubeId, returnSong, cb) {
  781. SongsModule.runJob("REQUEST_SONG", { youtubeId, userId: session.userId }, this)
  782. .then(response => {
  783. this.log(
  784. "SUCCESS",
  785. "SONGS_REQUEST",
  786. `User "${session.userId}" successfully requested song "${youtubeId}".`
  787. );
  788. return cb({
  789. status: "success",
  790. message: "Successfully requested that song",
  791. song: returnSong ? response.song : null
  792. });
  793. })
  794. .catch(async _err => {
  795. const err = await UtilsModule.runJob("GET_ERROR", { error: _err }, this);
  796. this.log(
  797. "ERROR",
  798. "SONGS_REQUEST",
  799. `Requesting song "${youtubeId}" failed for user ${session.userId}. "${err}"`
  800. );
  801. return cb({ status: "error", message: err, song: returnSong && _err.data ? _err.data.song : null });
  802. });
  803. }),
  804. /**
  805. * Verifies a song
  806. *
  807. * @param session
  808. * @param songId - the song id
  809. * @param cb
  810. */
  811. verify: isAdminRequired(async function add(session, songId, cb) {
  812. const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  813. async.waterfall(
  814. [
  815. next => {
  816. SongModel.findOne({ _id: songId }, next);
  817. },
  818. (song, next) => {
  819. if (!song) return next("This song is not in the database.");
  820. return next(null, song);
  821. },
  822. (song, next) => {
  823. const oldStatus = false;
  824. song.verifiedBy = session.userId;
  825. song.verifiedAt = Date.now();
  826. song.verified = true;
  827. song.save(err => next(err, song, oldStatus));
  828. },
  829. (song, oldStatus, next) => {
  830. song.genres.forEach(genre => {
  831. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
  832. .then(() => {})
  833. .catch(() => {});
  834. });
  835. SongsModule.runJob("UPDATE_SONG", { songId: song._id, oldStatus });
  836. next(null, song, oldStatus);
  837. }
  838. ],
  839. async err => {
  840. if (err) {
  841. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  842. this.log("ERROR", "SONGS_VERIFY", `User "${session.userId}" failed to verify song. "${err}"`);
  843. return cb({ status: "error", message: err });
  844. }
  845. this.log("SUCCESS", "SONGS_VERIFY", `User "${session.userId}" successfully verified song "${songId}".`);
  846. return cb({
  847. status: "success",
  848. message: "Song has been verified successfully."
  849. });
  850. }
  851. );
  852. // TODO Check if video is in queue and Add the song to the appropriate stations
  853. }),
  854. /**
  855. * Verify many songs
  856. *
  857. * @param session
  858. * @param songIds - array of song ids
  859. * @param cb
  860. */
  861. verifyMany: isAdminRequired(async function verifyMany(session, songIds, cb) {
  862. const successful = [];
  863. const failed = [];
  864. async.waterfall(
  865. [
  866. next => {
  867. async.eachLimit(
  868. songIds,
  869. 1,
  870. (songId, next) => {
  871. WSModule.runJob(
  872. "RUN_ACTION2",
  873. {
  874. session,
  875. namespace: "songs",
  876. action: "verify",
  877. args: [songId]
  878. },
  879. this
  880. )
  881. .then(res => {
  882. if (res.status === "error") failed.push(songId);
  883. else successful.push(songId);
  884. next();
  885. })
  886. .catch(err => {
  887. next(err);
  888. });
  889. },
  890. err => {
  891. if (err) next(err);
  892. else next();
  893. }
  894. );
  895. }
  896. ],
  897. async err => {
  898. if (err) {
  899. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  900. this.log("ERROR", "SONGS_VERIFY_MANY", `Failed to verify songs "${failed.join(", ")}". "${err}"`);
  901. return cb({ status: "error", message: err });
  902. }
  903. let message = "";
  904. if (successful.length === 1) message += `1 song has been successfully verified`;
  905. else message += `${successful.length} songs have been successfully verified`;
  906. if (failed.length > 0) {
  907. this.log("ERROR", "SONGS_VERIFY_MANY", `Failed to verify songs "${failed.join(", ")}". "${err}"`);
  908. if (failed.length === 1) message += `, failed to verify 1 song`;
  909. else message += `, failed to verify ${failed.length} songs`;
  910. }
  911. this.log("SUCCESS", "SONGS_VERIFY_MANY", `${message} "${successful.join(", ")}"`);
  912. return cb({
  913. status: "success",
  914. message
  915. });
  916. }
  917. );
  918. }),
  919. /**
  920. * Un-verifies a song
  921. *
  922. * @param session
  923. * @param songId - the song id
  924. * @param cb
  925. */
  926. unverify: isAdminRequired(async function add(session, songId, cb) {
  927. const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  928. async.waterfall(
  929. [
  930. next => {
  931. SongModel.findOne({ _id: songId }, next);
  932. },
  933. (song, next) => {
  934. if (!song) return next("This song is not in the database.");
  935. return next(null, song);
  936. },
  937. (song, next) => {
  938. song.verified = false;
  939. song.verifiedBy = null;
  940. song.verifiedAt = null;
  941. song.save(err => {
  942. next(err, song);
  943. });
  944. },
  945. (song, next) => {
  946. song.genres.forEach(genre => {
  947. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre })
  948. .then(() => {})
  949. .catch(() => {});
  950. });
  951. SongsModule.runJob("UPDATE_SONG", { songId, oldStatus: true });
  952. next(null);
  953. }
  954. ],
  955. async err => {
  956. if (err) {
  957. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  958. this.log("ERROR", "SONGS_UNVERIFY", `User "${session.userId}" failed to verify song. "${err}"`);
  959. return cb({ status: "error", message: err });
  960. }
  961. this.log(
  962. "SUCCESS",
  963. "SONGS_UNVERIFY",
  964. `User "${session.userId}" successfully unverified song "${songId}".`
  965. );
  966. return cb({
  967. status: "success",
  968. message: "Song has been unverified successfully."
  969. });
  970. }
  971. );
  972. // TODO Check if video is in queue and Add the song to the appropriate stations
  973. }),
  974. /**
  975. * Unverify many songs
  976. *
  977. * @param session
  978. * @param songIds - array of song ids
  979. * @param cb
  980. */
  981. unverifyMany: isAdminRequired(async function unverifyMany(session, songIds, cb) {
  982. const successful = [];
  983. const failed = [];
  984. async.waterfall(
  985. [
  986. next => {
  987. async.eachLimit(
  988. songIds,
  989. 1,
  990. (songId, next) => {
  991. WSModule.runJob(
  992. "RUN_ACTION2",
  993. {
  994. session,
  995. namespace: "songs",
  996. action: "unverify",
  997. args: [songId]
  998. },
  999. this
  1000. )
  1001. .then(res => {
  1002. if (res.status === "error") failed.push(songId);
  1003. else successful.push(songId);
  1004. next();
  1005. })
  1006. .catch(err => {
  1007. next(err);
  1008. });
  1009. },
  1010. err => {
  1011. if (err) next(err);
  1012. else next();
  1013. }
  1014. );
  1015. }
  1016. ],
  1017. async err => {
  1018. if (err) {
  1019. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1020. this.log(
  1021. "ERROR",
  1022. "SONGS_UNVERIFY_MANY",
  1023. `Failed to unverify songs "${failed.join(", ")}". "${err}"`
  1024. );
  1025. return cb({ status: "error", message: err });
  1026. }
  1027. let message = "";
  1028. if (successful.length === 1) message += `1 song has been successfully unverified`;
  1029. else message += `${successful.length} songs have been successfully unverified`;
  1030. if (failed.length > 0) {
  1031. this.log(
  1032. "ERROR",
  1033. "SONGS_UNVERIFY_MANY",
  1034. `Failed to unverify songs "${failed.join(", ")}". "${err}"`
  1035. );
  1036. if (failed.length === 1) message += `, failed to unverify 1 song`;
  1037. else message += `, failed to unverify ${failed.length} songs`;
  1038. }
  1039. this.log("SUCCESS", "SONGS_UNVERIFY_MANY", `${message} "${successful.join(", ")}"`);
  1040. return cb({
  1041. status: "success",
  1042. message
  1043. });
  1044. }
  1045. );
  1046. }),
  1047. /**
  1048. * Requests a set of songs
  1049. *
  1050. * @param {object} session - the session object automatically added by the websocket
  1051. * @param {string} url - the url of the the YouTube playlist
  1052. * @param {boolean} musicOnly - whether to only get music from the playlist
  1053. * @param {Function} cb - gets called with the result
  1054. */
  1055. requestSet: isLoginRequired(function requestSet(session, url, musicOnly, returnSongs, cb) {
  1056. async.waterfall(
  1057. [
  1058. next => {
  1059. YouTubeModule.runJob(
  1060. "GET_PLAYLIST",
  1061. {
  1062. url,
  1063. musicOnly
  1064. },
  1065. this
  1066. )
  1067. .then(res => {
  1068. next(null, res.songs);
  1069. })
  1070. .catch(next);
  1071. },
  1072. (youtubeIds, next) => {
  1073. let successful = 0;
  1074. let songs = {};
  1075. let failed = 0;
  1076. let alreadyInDatabase = 0;
  1077. if (youtubeIds.length === 0) next();
  1078. async.eachOfLimit(
  1079. youtubeIds,
  1080. 1,
  1081. (youtubeId, index, next) => {
  1082. WSModule.runJob(
  1083. "RUN_ACTION2",
  1084. {
  1085. session,
  1086. namespace: "songs",
  1087. action: "request",
  1088. args: [youtubeId, returnSongs]
  1089. },
  1090. this
  1091. )
  1092. .then(res => {
  1093. if (res.status === "success") successful += 1;
  1094. else failed += 1;
  1095. if (res.message === "This song is already in the database.") alreadyInDatabase += 1;
  1096. if (res.song) songs[index] = res.song;
  1097. else songs[index] = null;
  1098. })
  1099. .catch(() => {
  1100. failed += 1;
  1101. })
  1102. .finally(() => {
  1103. next();
  1104. });
  1105. },
  1106. () => {
  1107. if (returnSongs)
  1108. songs = Object.keys(songs)
  1109. .sort()
  1110. .map(key => songs[key]);
  1111. next(null, { successful, failed, alreadyInDatabase, songs });
  1112. }
  1113. );
  1114. }
  1115. ],
  1116. async (err, response) => {
  1117. if (err) {
  1118. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1119. this.log(
  1120. "ERROR",
  1121. "REQUEST_SET",
  1122. `Importing a YouTube playlist to be requested failed for user "${session.userId}". "${err}"`
  1123. );
  1124. return cb({ status: "error", message: err });
  1125. }
  1126. this.log(
  1127. "SUCCESS",
  1128. "REQUEST_SET",
  1129. `Successfully imported a YouTube playlist to be requested for user "${session.userId}".`
  1130. );
  1131. return cb({
  1132. status: "success",
  1133. message: `Playlist is done importing. ${response.successful} were added succesfully, ${response.failed} failed (${response.alreadyInDatabase} were already in database)`,
  1134. songs: returnSongs ? response.songs : null
  1135. });
  1136. }
  1137. );
  1138. }),
  1139. /**
  1140. * Likes a song
  1141. *
  1142. * @param session
  1143. * @param youtubeId - the youtube id
  1144. * @param cb
  1145. */
  1146. like: isLoginRequired(async function like(session, youtubeId, cb) {
  1147. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1148. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1149. async.waterfall(
  1150. [
  1151. next => songModel.findOne({ youtubeId }, next),
  1152. (song, next) => {
  1153. if (!song) return next("No song found with that id.");
  1154. return next(null, song);
  1155. },
  1156. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  1157. (song, user, next) => {
  1158. if (!user) return next("User does not exist.");
  1159. return this.module
  1160. .runJob(
  1161. "RUN_ACTION2",
  1162. {
  1163. session,
  1164. namespace: "playlists",
  1165. action: "removeSongFromPlaylist",
  1166. args: [youtubeId, user.dislikedSongsPlaylist]
  1167. },
  1168. this
  1169. )
  1170. .then(res => {
  1171. if (res.status === "error")
  1172. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  1173. return next(null, song, user.likedSongsPlaylist);
  1174. })
  1175. .catch(err => next(err));
  1176. },
  1177. (song, likedSongsPlaylist, next) =>
  1178. this.module
  1179. .runJob(
  1180. "RUN_ACTION2",
  1181. {
  1182. session,
  1183. namespace: "playlists",
  1184. action: "addSongToPlaylist",
  1185. args: [false, youtubeId, likedSongsPlaylist]
  1186. },
  1187. this
  1188. )
  1189. .then(res => {
  1190. if (res.status === "error") {
  1191. if (res.message === "That song is already in the playlist")
  1192. return next("You have already liked this song.");
  1193. return next("Unable to add song to the 'Liked Songs' playlist.");
  1194. }
  1195. return next(null, song);
  1196. })
  1197. .catch(err => next(err)),
  1198. (song, next) => {
  1199. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1200. .then(ratings => next(null, song, ratings))
  1201. .catch(err => next(err));
  1202. }
  1203. ],
  1204. async (err, song, ratings) => {
  1205. if (err) {
  1206. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1207. this.log(
  1208. "ERROR",
  1209. "SONGS_LIKE",
  1210. `User "${session.userId}" failed to like song ${youtubeId}. "${err}"`
  1211. );
  1212. return cb({ status: "error", message: err });
  1213. }
  1214. const { likes, dislikes } = ratings;
  1215. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1216. CacheModule.runJob("PUB", {
  1217. channel: "song.like",
  1218. value: JSON.stringify({
  1219. youtubeId,
  1220. userId: session.userId,
  1221. likes,
  1222. dislikes
  1223. })
  1224. });
  1225. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1226. userId: session.userId,
  1227. type: "song__like",
  1228. payload: {
  1229. message: `Liked song <youtubeId>${song.title} by ${song.artists.join(", ")}</youtubeId>`,
  1230. youtubeId,
  1231. thumbnail: song.thumbnail
  1232. }
  1233. });
  1234. return cb({
  1235. status: "success",
  1236. message: "You have successfully liked this song."
  1237. });
  1238. }
  1239. );
  1240. }),
  1241. /**
  1242. * Dislikes a song
  1243. *
  1244. * @param session
  1245. * @param youtubeId - the youtube id
  1246. * @param cb
  1247. */
  1248. dislike: isLoginRequired(async function dislike(session, youtubeId, cb) {
  1249. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1250. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1251. async.waterfall(
  1252. [
  1253. next => {
  1254. songModel.findOne({ youtubeId }, next);
  1255. },
  1256. (song, next) => {
  1257. if (!song) return next("No song found with that id.");
  1258. return next(null, song);
  1259. },
  1260. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  1261. (song, user, next) => {
  1262. if (!user) return next("User does not exist.");
  1263. return this.module
  1264. .runJob(
  1265. "RUN_ACTION2",
  1266. {
  1267. session,
  1268. namespace: "playlists",
  1269. action: "removeSongFromPlaylist",
  1270. args: [youtubeId, user.likedSongsPlaylist]
  1271. },
  1272. this
  1273. )
  1274. .then(res => {
  1275. if (res.status === "error")
  1276. return next("Unable to remove song from the 'Liked Songs' playlist.");
  1277. return next(null, song, user.dislikedSongsPlaylist);
  1278. })
  1279. .catch(err => next(err));
  1280. },
  1281. (song, dislikedSongsPlaylist, next) =>
  1282. this.module
  1283. .runJob(
  1284. "RUN_ACTION2",
  1285. {
  1286. session,
  1287. namespace: "playlists",
  1288. action: "addSongToPlaylist",
  1289. args: [false, youtubeId, dislikedSongsPlaylist]
  1290. },
  1291. this
  1292. )
  1293. .then(res => {
  1294. if (res.status === "error") {
  1295. if (res.message === "That song is already in the playlist")
  1296. return next("You have already disliked this song.");
  1297. return next("Unable to add song to the 'Disliked Songs' playlist.");
  1298. }
  1299. return next(null, song);
  1300. })
  1301. .catch(err => next(err)),
  1302. (song, next) => {
  1303. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1304. .then(ratings => next(null, song, ratings))
  1305. .catch(err => next(err));
  1306. }
  1307. ],
  1308. async (err, song, ratings) => {
  1309. if (err) {
  1310. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1311. this.log(
  1312. "ERROR",
  1313. "SONGS_DISLIKE",
  1314. `User "${session.userId}" failed to dislike song ${youtubeId}. "${err}"`
  1315. );
  1316. return cb({ status: "error", message: err });
  1317. }
  1318. const { likes, dislikes } = ratings;
  1319. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1320. CacheModule.runJob("PUB", {
  1321. channel: "song.dislike",
  1322. value: JSON.stringify({
  1323. youtubeId,
  1324. userId: session.userId,
  1325. likes,
  1326. dislikes
  1327. })
  1328. });
  1329. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1330. userId: session.userId,
  1331. type: "song__dislike",
  1332. payload: {
  1333. message: `Disliked song <youtubeId>${song.title} by ${song.artists.join(", ")}</youtubeId>`,
  1334. youtubeId,
  1335. thumbnail: song.thumbnail
  1336. }
  1337. });
  1338. return cb({
  1339. status: "success",
  1340. message: "You have successfully disliked this song."
  1341. });
  1342. }
  1343. );
  1344. }),
  1345. /**
  1346. * Undislikes a song
  1347. *
  1348. * @param session
  1349. * @param youtubeId - the youtube id
  1350. * @param cb
  1351. */
  1352. undislike: isLoginRequired(async function undislike(session, youtubeId, cb) {
  1353. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1354. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1355. async.waterfall(
  1356. [
  1357. next => {
  1358. songModel.findOne({ youtubeId }, next);
  1359. },
  1360. (song, next) => {
  1361. if (!song) return next("No song found with that id.");
  1362. return next(null, song);
  1363. },
  1364. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  1365. (song, user, next) => {
  1366. if (!user) return next("User does not exist.");
  1367. return this.module
  1368. .runJob(
  1369. "RUN_ACTION2",
  1370. {
  1371. session,
  1372. namespace: "playlists",
  1373. action: "removeSongFromPlaylist",
  1374. args: [youtubeId, user.dislikedSongsPlaylist]
  1375. },
  1376. this
  1377. )
  1378. .then(res => {
  1379. if (res.status === "error")
  1380. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  1381. return next(null, song, user.likedSongsPlaylist);
  1382. })
  1383. .catch(err => next(err));
  1384. },
  1385. (song, likedSongsPlaylist, next) => {
  1386. this.module
  1387. .runJob(
  1388. "RUN_ACTION2",
  1389. {
  1390. session,
  1391. namespace: "playlists",
  1392. action: "removeSongFromPlaylist",
  1393. args: [youtubeId, likedSongsPlaylist]
  1394. },
  1395. this
  1396. )
  1397. .then(res => {
  1398. if (res.status === "error")
  1399. return next("Unable to remove song from the 'Liked Songs' playlist.");
  1400. return next(null, song);
  1401. })
  1402. .catch(err => next(err));
  1403. },
  1404. (song, next) => {
  1405. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1406. .then(ratings => next(null, song, ratings))
  1407. .catch(err => next(err));
  1408. }
  1409. ],
  1410. async (err, song, ratings) => {
  1411. if (err) {
  1412. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1413. this.log(
  1414. "ERROR",
  1415. "SONGS_UNDISLIKE",
  1416. `User "${session.userId}" failed to undislike song ${youtubeId}. "${err}"`
  1417. );
  1418. return cb({ status: "error", message: err });
  1419. }
  1420. const { likes, dislikes } = ratings;
  1421. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1422. CacheModule.runJob("PUB", {
  1423. channel: "song.undislike",
  1424. value: JSON.stringify({
  1425. youtubeId,
  1426. userId: session.userId,
  1427. likes,
  1428. dislikes
  1429. })
  1430. });
  1431. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1432. userId: session.userId,
  1433. type: "song__undislike",
  1434. payload: {
  1435. message: `Removed <youtubeId>${song.title} by ${song.artists.join(
  1436. ", "
  1437. )}</youtubeId> from your Disliked Songs`,
  1438. youtubeId,
  1439. thumbnail: song.thumbnail
  1440. }
  1441. });
  1442. return cb({
  1443. status: "success",
  1444. message: "You have successfully undisliked this song."
  1445. });
  1446. }
  1447. );
  1448. }),
  1449. /**
  1450. * Unlikes a song
  1451. *
  1452. * @param session
  1453. * @param youtubeId - the youtube id
  1454. * @param cb
  1455. */
  1456. unlike: isLoginRequired(async function unlike(session, youtubeId, cb) {
  1457. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  1458. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1459. async.waterfall(
  1460. [
  1461. next => {
  1462. songModel.findOne({ youtubeId }, next);
  1463. },
  1464. (song, next) => {
  1465. if (!song) return next("No song found with that id.");
  1466. return next(null, song);
  1467. },
  1468. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  1469. (song, user, next) => {
  1470. if (!user) return next("User does not exist.");
  1471. return this.module
  1472. .runJob(
  1473. "RUN_ACTION2",
  1474. {
  1475. session,
  1476. namespace: "playlists",
  1477. action: "removeSongFromPlaylist",
  1478. args: [youtubeId, user.dislikedSongsPlaylist]
  1479. },
  1480. this
  1481. )
  1482. .then(res => {
  1483. if (res.status === "error")
  1484. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  1485. return next(null, song, user.likedSongsPlaylist);
  1486. })
  1487. .catch(err => next(err));
  1488. },
  1489. (song, likedSongsPlaylist, next) => {
  1490. this.module
  1491. .runJob(
  1492. "RUN_ACTION2",
  1493. {
  1494. session,
  1495. namespace: "playlists",
  1496. action: "removeSongFromPlaylist",
  1497. args: [youtubeId, likedSongsPlaylist]
  1498. },
  1499. this
  1500. )
  1501. .then(res => {
  1502. if (res.status === "error")
  1503. return next("Unable to remove song from the 'Liked Songs' playlist.");
  1504. return next(null, song);
  1505. })
  1506. .catch(err => next(err));
  1507. },
  1508. (song, next) => {
  1509. SongsModule.runJob("RECALCULATE_SONG_RATINGS", { songId: song._id, youtubeId })
  1510. .then(ratings => next(null, song, ratings))
  1511. .catch(err => next(err));
  1512. }
  1513. ],
  1514. async (err, song, ratings) => {
  1515. if (err) {
  1516. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1517. this.log(
  1518. "ERROR",
  1519. "SONGS_UNLIKE",
  1520. `User "${session.userId}" failed to unlike song ${youtubeId}. "${err}"`
  1521. );
  1522. return cb({ status: "error", message: err });
  1523. }
  1524. const { likes, dislikes } = ratings;
  1525. SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  1526. CacheModule.runJob("PUB", {
  1527. channel: "song.unlike",
  1528. value: JSON.stringify({
  1529. youtubeId,
  1530. userId: session.userId,
  1531. likes,
  1532. dislikes
  1533. })
  1534. });
  1535. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1536. userId: session.userId,
  1537. type: "song__unlike",
  1538. payload: {
  1539. message: `Removed <youtubeId>${song.title} by ${song.artists.join(
  1540. ", "
  1541. )}</youtubeId> from your Liked Songs`,
  1542. youtubeId,
  1543. thumbnail: song.thumbnail
  1544. }
  1545. });
  1546. return cb({
  1547. status: "success",
  1548. message: "You have successfully unliked this song."
  1549. });
  1550. }
  1551. );
  1552. }),
  1553. /**
  1554. * Gets song ratings
  1555. *
  1556. * @param session
  1557. * @param songId - the Musare song id
  1558. * @param cb
  1559. */
  1560. getSongRatings: isLoginRequired(async function getSongRatings(session, songId, cb) {
  1561. async.waterfall(
  1562. [
  1563. next => {
  1564. SongsModule.runJob("GET_SONG", { songId }, this)
  1565. .then(res => next(null, res.song))
  1566. .catch(next);
  1567. },
  1568. (song, next) => {
  1569. next(null, {
  1570. likes: song.likes,
  1571. dislikes: song.dislikes
  1572. });
  1573. }
  1574. ],
  1575. async (err, ratings) => {
  1576. if (err) {
  1577. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1578. this.log(
  1579. "ERROR",
  1580. "SONGS_GET_RATINGS",
  1581. `User "${session.userId}" failed to get ratings for ${songId}. "${err}"`
  1582. );
  1583. return cb({ status: "error", message: err });
  1584. }
  1585. const { likes, dislikes } = ratings;
  1586. return cb({
  1587. status: "success",
  1588. data: {
  1589. likes,
  1590. dislikes
  1591. }
  1592. });
  1593. }
  1594. );
  1595. }),
  1596. /**
  1597. * Gets user's own song ratings
  1598. *
  1599. * @param session
  1600. * @param youtubeId - the youtube id
  1601. * @param cb
  1602. */
  1603. getOwnSongRatings: isLoginRequired(async function getOwnSongRatings(session, youtubeId, cb) {
  1604. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  1605. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1606. async.waterfall(
  1607. [
  1608. next => songModel.findOne({ youtubeId }, next),
  1609. (song, next) => {
  1610. if (!song) return next("No song found with that id.");
  1611. return next(null);
  1612. },
  1613. next =>
  1614. playlistModel.findOne(
  1615. { createdBy: session.userId, displayName: "Liked Songs" },
  1616. (err, playlist) => {
  1617. if (err) return next(err);
  1618. if (!playlist) return next("'Liked Songs' playlist does not exist.");
  1619. let isLiked = false;
  1620. Object.values(playlist.songs).forEach(song => {
  1621. // song is found in 'liked songs' playlist
  1622. if (song.youtubeId === youtubeId) isLiked = true;
  1623. });
  1624. return next(null, isLiked);
  1625. }
  1626. ),
  1627. (isLiked, next) =>
  1628. playlistModel.findOne(
  1629. { createdBy: session.userId, displayName: "Disliked Songs" },
  1630. (err, playlist) => {
  1631. if (err) return next(err);
  1632. if (!playlist) return next("'Disliked Songs' playlist does not exist.");
  1633. const ratings = { isLiked, isDisliked: false };
  1634. Object.values(playlist.songs).forEach(song => {
  1635. // song is found in 'disliked songs' playlist
  1636. if (song.youtubeId === youtubeId) ratings.isDisliked = true;
  1637. });
  1638. return next(null, ratings);
  1639. }
  1640. )
  1641. ],
  1642. async (err, ratings) => {
  1643. if (err) {
  1644. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1645. this.log(
  1646. "ERROR",
  1647. "SONGS_GET_OWN_RATINGS",
  1648. `User "${session.userId}" failed to get ratings for ${youtubeId}. "${err}"`
  1649. );
  1650. return cb({ status: "error", message: err });
  1651. }
  1652. const { isLiked, isDisliked } = ratings;
  1653. return cb({
  1654. status: "success",
  1655. data: {
  1656. youtubeId,
  1657. liked: isLiked,
  1658. disliked: isDisliked
  1659. }
  1660. });
  1661. }
  1662. );
  1663. }),
  1664. /**
  1665. * Gets a list of all genres
  1666. *
  1667. * @param session
  1668. * @param cb
  1669. */
  1670. getGenres: isAdminRequired(function getGenres(session, cb) {
  1671. async.waterfall(
  1672. [
  1673. next => {
  1674. SongsModule.runJob("GET_GENRES", this)
  1675. .then(res => {
  1676. next(null, res.genres);
  1677. })
  1678. .catch(next);
  1679. }
  1680. ],
  1681. async (err, genres) => {
  1682. if (err && err !== true) {
  1683. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1684. this.log("ERROR", "GET_GENRES", `User ${session.userId} failed to get genres. '${err}'`);
  1685. cb({ status: "error", message: err });
  1686. } else {
  1687. this.log("SUCCESS", "GET_GENRES", `User ${session.userId} has successfully got the genres.`);
  1688. cb({
  1689. status: "success",
  1690. message: "Successfully got genres.",
  1691. data: {
  1692. items: genres
  1693. }
  1694. });
  1695. }
  1696. }
  1697. );
  1698. }),
  1699. /**
  1700. * Bulk update genres for selected songs
  1701. *
  1702. * @param session
  1703. * @param method Whether to add, remove or replace genres
  1704. * @param genres Array of genres to apply
  1705. * @param songIds Array of songIds to apply genres to
  1706. * @param cb
  1707. */
  1708. editGenres: isAdminRequired(async function editGenres(session, method, genres, songIds, cb) {
  1709. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1710. async.waterfall(
  1711. [
  1712. next => {
  1713. songModel.find({ _id: { $in: songIds } }, next);
  1714. },
  1715. (songs, next) => {
  1716. const songsFound = songs.map(song => song._id);
  1717. if (songsFound.length > 0) next(null, songsFound);
  1718. else next("None of the specified songs were found.");
  1719. },
  1720. (songsFound, next) => {
  1721. const query = {};
  1722. if (method === "add") {
  1723. query.$push = { genres: { $each: genres } };
  1724. } else if (method === "remove") {
  1725. query.$pullAll = { genres };
  1726. } else if (method === "replace") {
  1727. query.$set = { genres };
  1728. } else {
  1729. next("Invalid method.");
  1730. }
  1731. songModel.updateMany({ _id: { $in: songsFound } }, query, { runValidators: true }, err => {
  1732. if (err) next(err);
  1733. SongsModule.runJob("UPDATE_SONGS", { songIds: songsFound });
  1734. });
  1735. }
  1736. ],
  1737. async err => {
  1738. if (err && err !== true) {
  1739. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1740. this.log("ERROR", "EDIT_GENRES", `User ${session.userId} failed to edit genres. '${err}'`);
  1741. cb({ status: "error", message: err });
  1742. } else {
  1743. this.log("SUCCESS", "EDIT_GENRES", `User ${session.userId} has successfully edited genres.`);
  1744. cb({
  1745. status: "success",
  1746. message: "Successfully edited genres."
  1747. });
  1748. }
  1749. }
  1750. );
  1751. }),
  1752. /**
  1753. * Gets a list of all artists
  1754. *
  1755. * @param session
  1756. * @param cb
  1757. */
  1758. getArtists: isAdminRequired(function getArtists(session, cb) {
  1759. async.waterfall(
  1760. [
  1761. next => {
  1762. SongsModule.runJob("GET_ARTISTS", this)
  1763. .then(res => {
  1764. next(null, res.artists);
  1765. })
  1766. .catch(next);
  1767. }
  1768. ],
  1769. async (err, artists) => {
  1770. if (err && err !== true) {
  1771. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1772. this.log("ERROR", "GET_ARTISTS", `User ${session.userId} failed to get artists. '${err}'`);
  1773. cb({ status: "error", message: err });
  1774. } else {
  1775. this.log("SUCCESS", "GET_ARTISTS", `User ${session.userId} has successfully got the artists.`);
  1776. cb({
  1777. status: "success",
  1778. message: "Successfully got artists.",
  1779. data: {
  1780. items: artists
  1781. }
  1782. });
  1783. }
  1784. }
  1785. );
  1786. }),
  1787. /**
  1788. * Bulk update artists for selected songs
  1789. *
  1790. * @param session
  1791. * @param method Whether to add, remove or replace artists
  1792. * @param artists Array of artists to apply
  1793. * @param songIds Array of songIds to apply artists to
  1794. * @param cb
  1795. */
  1796. editArtists: isAdminRequired(async function editArtists(session, method, artists, songIds, cb) {
  1797. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1798. async.waterfall(
  1799. [
  1800. next => {
  1801. songModel.find({ _id: { $in: songIds } }, next);
  1802. },
  1803. (songs, next) => {
  1804. const songsFound = songs.map(song => song._id);
  1805. if (songsFound.length > 0) next(null, songsFound);
  1806. else next("None of the specified songs were found.");
  1807. },
  1808. (songsFound, next) => {
  1809. const query = {};
  1810. if (method === "add") {
  1811. query.$push = { artists: { $each: artists } };
  1812. } else if (method === "remove") {
  1813. query.$pullAll = { artists };
  1814. } else if (method === "replace") {
  1815. query.$set = { artists };
  1816. } else {
  1817. next("Invalid method.");
  1818. }
  1819. songModel.updateMany({ _id: { $in: songsFound } }, query, { runValidators: true }, err => {
  1820. if (err) next(err);
  1821. SongsModule.runJob("UPDATE_SONGS", { songIds: songsFound });
  1822. });
  1823. }
  1824. ],
  1825. async err => {
  1826. if (err && err !== true) {
  1827. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1828. this.log("ERROR", "EDIT_ARTISTS", `User ${session.userId} failed to edit artists. '${err}'`);
  1829. cb({ status: "error", message: err });
  1830. } else {
  1831. this.log("SUCCESS", "EDIT_ARTISTS", `User ${session.userId} has successfully edited artists.`);
  1832. cb({
  1833. status: "success",
  1834. message: "Successfully edited artists."
  1835. });
  1836. }
  1837. }
  1838. );
  1839. }),
  1840. /**
  1841. * Gets a list of all tags
  1842. *
  1843. * @param session
  1844. * @param cb
  1845. */
  1846. getTags: isAdminRequired(function getTags(session, cb) {
  1847. async.waterfall(
  1848. [
  1849. next => {
  1850. SongsModule.runJob("GET_TAGS", this)
  1851. .then(res => {
  1852. next(null, res.tags);
  1853. })
  1854. .catch(next);
  1855. }
  1856. ],
  1857. async (err, tags) => {
  1858. if (err && err !== true) {
  1859. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1860. this.log("ERROR", "GET_TAGS", `User ${session.userId} failed to get tags. '${err}'`);
  1861. cb({ status: "error", message: err });
  1862. } else {
  1863. this.log("SUCCESS", "GET_TAGS", `User ${session.userId} has successfully got the tags.`);
  1864. cb({
  1865. status: "success",
  1866. message: "Successfully got tags.",
  1867. data: {
  1868. items: tags
  1869. }
  1870. });
  1871. }
  1872. }
  1873. );
  1874. }),
  1875. /**
  1876. * Bulk update tags for selected songs
  1877. *
  1878. * @param session
  1879. * @param method Whether to add, remove or replace tags
  1880. * @param tags Array of tags to apply
  1881. * @param songIds Array of songIds to apply tags to
  1882. * @param cb
  1883. */
  1884. editTags: isAdminRequired(async function editTags(session, method, tags, songIds, cb) {
  1885. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1886. async.waterfall(
  1887. [
  1888. next => {
  1889. songModel.find({ _id: { $in: songIds } }, next);
  1890. },
  1891. (songs, next) => {
  1892. const songsFound = songs.map(song => song._id);
  1893. if (songsFound.length > 0) next(null, songsFound);
  1894. else next("None of the specified songs were found.");
  1895. },
  1896. (songsFound, next) => {
  1897. const query = {};
  1898. if (method === "add") {
  1899. query.$push = { tags: { $each: tags } };
  1900. } else if (method === "remove") {
  1901. query.$pullAll = { tags };
  1902. } else if (method === "replace") {
  1903. query.$set = { tags };
  1904. } else {
  1905. next("Invalid method.");
  1906. }
  1907. songModel.updateMany({ _id: { $in: songsFound } }, query, { runValidators: true }, err => {
  1908. if (err) next(err);
  1909. SongsModule.runJob("UPDATE_SONGS", { songIds: songsFound });
  1910. });
  1911. }
  1912. ],
  1913. async err => {
  1914. if (err && err !== true) {
  1915. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1916. this.log("ERROR", "EDIT_TAGS", `User ${session.userId} failed to edit tags. '${err}'`);
  1917. cb({ status: "error", message: err });
  1918. } else {
  1919. this.log("SUCCESS", "EDIT_TAGS", `User ${session.userId} has successfully edited tags.`);
  1920. cb({
  1921. status: "success",
  1922. message: "Successfully edited tags."
  1923. });
  1924. }
  1925. }
  1926. );
  1927. })
  1928. };