songs.js 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258
  1. import async from "async";
  2. import { isAdminRequired, isLoginRequired } from "./hooks";
  3. // eslint-disable-next-line
  4. import moduleManager from "../../index";
  5. const DBModule = moduleManager.modules.db;
  6. const UtilsModule = moduleManager.modules.utils;
  7. const WSModule = moduleManager.modules.ws;
  8. const CacheModule = moduleManager.modules.cache;
  9. const SongsModule = moduleManager.modules.songs;
  10. const PlaylistsModule = moduleManager.modules.playlists;
  11. const StationsModule = moduleManager.modules.stations;
  12. CacheModule.runJob("SUB", {
  13. channel: "song.updated",
  14. cb: async data => {
  15. const songModel = await DBModule.runJob("GET_MODEL", {
  16. modelName: "song"
  17. });
  18. songModel.findOne({ _id: data.songId }, (err, song) => {
  19. WSModule.runJob("EMIT_TO_ROOMS", {
  20. rooms: ["import-album", "admin.songs", `edit-song.${data.songId}`, "edit-songs"],
  21. args: ["event:admin.song.updated", { data: { song, oldStatus: data.oldStatus } }]
  22. });
  23. });
  24. }
  25. });
  26. CacheModule.runJob("SUB", {
  27. channel: "song.removed",
  28. cb: async data => {
  29. WSModule.runJob("EMIT_TO_ROOMS", {
  30. rooms: ["import-album", "admin.songs", `edit-song.${data.songId}`, "edit-songs"],
  31. args: ["event:admin.song.removed", { data }]
  32. });
  33. }
  34. });
  35. export default {
  36. /**
  37. * Returns the length of the songs list
  38. *
  39. * @param {object} session - the session object automatically added by the websocket
  40. * @param cb
  41. */
  42. length: isAdminRequired(async function length(session, cb) {
  43. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  44. async.waterfall(
  45. [
  46. next => {
  47. songModel.countDocuments({}, next);
  48. }
  49. ],
  50. async (err, count) => {
  51. if (err) {
  52. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  53. this.log("ERROR", "SONGS_LENGTH", `Failed to get length from songs. "${err}"`);
  54. return cb({ status: "error", message: err });
  55. }
  56. this.log("SUCCESS", "SONGS_LENGTH", `Got length from songs successfully.`);
  57. return cb({ status: "success", message: "Successfully got length of songs.", data: { length: count } });
  58. }
  59. );
  60. }),
  61. /**
  62. * Gets songs, used in the admin songs page by the AdvancedTable component
  63. *
  64. * @param {object} session - the session object automatically added by the websocket
  65. * @param page - the page
  66. * @param pageSize - the size per page
  67. * @param properties - the properties to return for each song
  68. * @param sort - the sort object
  69. * @param queries - the queries array
  70. * @param operator - the operator for queries
  71. * @param cb
  72. */
  73. getData: isAdminRequired(async function getSet(session, page, pageSize, properties, sort, queries, operator, cb) {
  74. async.waterfall(
  75. [
  76. next => {
  77. DBModule.runJob(
  78. "GET_DATA",
  79. {
  80. page,
  81. pageSize,
  82. properties,
  83. sort,
  84. queries,
  85. operator,
  86. modelName: "song",
  87. blacklistedProperties: [],
  88. specialProperties: {
  89. requestedBy: [
  90. {
  91. $addFields: {
  92. requestedByOID: {
  93. $convert: {
  94. input: "$requestedBy",
  95. to: "objectId",
  96. onError: "unknown",
  97. onNull: "unknown"
  98. }
  99. }
  100. }
  101. },
  102. {
  103. $lookup: {
  104. from: "users",
  105. localField: "requestedByOID",
  106. foreignField: "_id",
  107. as: "requestedByUser"
  108. }
  109. },
  110. {
  111. $addFields: {
  112. requestedByUsername: {
  113. $ifNull: ["$requestedByUser.username", "unknown"]
  114. }
  115. }
  116. },
  117. {
  118. $project: {
  119. requestedByOID: 0,
  120. requestedByUser: 0
  121. }
  122. }
  123. ],
  124. verifiedBy: [
  125. {
  126. $addFields: {
  127. verifiedByOID: {
  128. $convert: {
  129. input: "$verifiedBy",
  130. to: "objectId",
  131. onError: "unknown",
  132. onNull: "unknown"
  133. }
  134. }
  135. }
  136. },
  137. {
  138. $lookup: {
  139. from: "users",
  140. localField: "verifiedByOID",
  141. foreignField: "_id",
  142. as: "verifiedByUser"
  143. }
  144. },
  145. {
  146. $unwind: {
  147. path: "$verifiedByUser",
  148. preserveNullAndEmptyArrays: true
  149. }
  150. },
  151. {
  152. $addFields: {
  153. verifiedByUsername: {
  154. $ifNull: ["$verifiedByUser.username", "unknown"]
  155. }
  156. }
  157. },
  158. {
  159. $project: {
  160. verifiedByOID: 0,
  161. verifiedByUser: 0
  162. }
  163. }
  164. ]
  165. },
  166. specialQueries: {
  167. requestedBy: newQuery => ({
  168. $or: [newQuery, { requestedByUsername: newQuery.requestedBy }]
  169. }),
  170. verifiedBy: newQuery => ({
  171. $or: [newQuery, { verifiedByUsername: newQuery.verifiedBy }]
  172. })
  173. }
  174. },
  175. this
  176. )
  177. .then(response => {
  178. next(null, response);
  179. })
  180. .catch(err => {
  181. next(err);
  182. });
  183. }
  184. ],
  185. async (err, response) => {
  186. if (err) {
  187. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  188. this.log("ERROR", "SONGS_GET_DATA", `Failed to get data from songs. "${err}"`);
  189. return cb({ status: "error", message: err });
  190. }
  191. this.log("SUCCESS", "SONGS_GET_DATA", `Got data from songs successfully.`);
  192. return cb({ status: "success", message: "Successfully got data from songs.", data: response });
  193. }
  194. );
  195. }),
  196. /**
  197. * Updates all songs
  198. *
  199. * @param {object} session - the session object automatically added by the websocket
  200. * @param cb
  201. */
  202. updateAll: isAdminRequired(async function updateAll(session, cb) {
  203. async.waterfall(
  204. [
  205. next => {
  206. SongsModule.runJob("UPDATE_ALL_SONGS", {}, this)
  207. .then(() => {
  208. next();
  209. })
  210. .catch(err => {
  211. next(err);
  212. });
  213. }
  214. ],
  215. async err => {
  216. if (err) {
  217. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  218. this.log("ERROR", "SONGS_UPDATE_ALL", `Failed to update all songs. "${err}"`);
  219. return cb({ status: "error", message: err });
  220. }
  221. this.log("SUCCESS", "SONGS_UPDATE_ALL", `Updated all songs successfully.`);
  222. return cb({ status: "success", message: "Successfully updated all songs." });
  223. }
  224. );
  225. }),
  226. /**
  227. * Gets a song from the Musare song id
  228. *
  229. * @param {object} session - the session object automatically added by the websocket
  230. * @param {string} songId - the song id
  231. * @param {Function} cb
  232. */
  233. getSongFromSongId: isAdminRequired(function getSongFromSongId(session, songId, cb) {
  234. async.waterfall(
  235. [
  236. next => {
  237. SongsModule.runJob("GET_SONG", { songId }, this)
  238. .then(response => next(null, response.song))
  239. .catch(err => next(err));
  240. }
  241. ],
  242. async (err, song) => {
  243. if (err) {
  244. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  245. this.log("ERROR", "SONGS_GET_SONG_FROM_MUSARE_ID", `Failed to get song ${songId}. "${err}"`);
  246. return cb({ status: "error", message: err });
  247. }
  248. this.log("SUCCESS", "SONGS_GET_SONG_FROM_MUSARE_ID", `Got song ${songId} successfully.`);
  249. return cb({ status: "success", data: { song } });
  250. }
  251. );
  252. }),
  253. /**
  254. * Gets multiple songs from the Musare song ids
  255. * At this time only used in EditSongs
  256. *
  257. * @param {object} session - the session object automatically added by the websocket
  258. * @param {Array} songIds - the song ids
  259. * @param {Function} cb
  260. */
  261. getSongsFromSongIds: isAdminRequired(function getSongFromSongId(session, songIds, cb) {
  262. async.waterfall(
  263. [
  264. next => {
  265. SongsModule.runJob(
  266. "GET_SONGS",
  267. {
  268. songIds,
  269. properties: ["youtubeId", "title", "artists", "thumbnail", "duration", "verified", "_id"]
  270. },
  271. this
  272. )
  273. .then(response => next(null, response.songs))
  274. .catch(err => next(err));
  275. }
  276. ],
  277. async (err, songs) => {
  278. if (err) {
  279. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  280. this.log("ERROR", "SONGS_GET_SONGS_FROM_MUSARE_IDS", `Failed to get songs. "${err}"`);
  281. return cb({ status: "error", message: err });
  282. }
  283. this.log("SUCCESS", "SONGS_GET_SONGS_FROM_MUSARE_IDS", `Got songs successfully.`);
  284. return cb({ status: "success", data: { songs } });
  285. }
  286. );
  287. }),
  288. /**
  289. * Creates a song
  290. *
  291. * @param {object} session - the session object automatically added by the websocket
  292. * @param {object} newSong - the song object
  293. * @param {Function} cb
  294. */
  295. create: isAdminRequired(async function create(session, newSong, cb) {
  296. async.waterfall(
  297. [
  298. next => {
  299. SongsModule.runJob("CREATE_SONG", { song: newSong, userId: session.userId }, this)
  300. .then(song => next(null, song))
  301. .catch(next);
  302. }
  303. ],
  304. async (err, song) => {
  305. if (err) {
  306. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  307. this.log("ERROR", "SONGS_CREATE", `Failed to create song "${JSON.stringify(newSong)}". "${err}"`);
  308. return cb({ status: "error", message: err });
  309. }
  310. this.log("SUCCESS", "SONGS_CREATE", `Successfully created song "${song._id}".`);
  311. return cb({
  312. status: "success",
  313. message: "Song has been successfully created",
  314. data: { song }
  315. });
  316. }
  317. );
  318. }),
  319. /**
  320. * Updates a song
  321. *
  322. * @param {object} session - the session object automatically added by the websocket
  323. * @param {string} songId - the song id
  324. * @param {object} song - the updated song object
  325. * @param {Function} cb
  326. */
  327. update: isAdminRequired(async function update(session, songId, song, cb) {
  328. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  329. let existingSong = null;
  330. async.waterfall(
  331. [
  332. next => {
  333. songModel.findOne({ _id: songId }, next);
  334. },
  335. (_existingSong, next) => {
  336. existingSong = _existingSong;
  337. // Verify the song
  338. if (existingSong.verified === false && song.verified === true) {
  339. song.verifiedBy = session.userId;
  340. song.verifiedAt = Date.now();
  341. }
  342. // Unverify the song
  343. else if (existingSong.verified === true && song.verified === false) {
  344. song.verifiedBy = null;
  345. song.verifiedAt = null;
  346. }
  347. next();
  348. },
  349. next => {
  350. songModel.updateOne({ _id: songId }, song, { runValidators: true }, next);
  351. },
  352. (res, next) => {
  353. SongsModule.runJob("UPDATE_SONG", { songId }, this)
  354. .then(song => {
  355. existingSong.genres
  356. .concat(song.genres)
  357. .filter((value, index, self) => self.indexOf(value) === index)
  358. .forEach(genre => {
  359. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", {
  360. genre,
  361. createPlaylist: song.verified
  362. })
  363. .then(() => {})
  364. .catch(() => {});
  365. });
  366. next(null, song);
  367. })
  368. .catch(next);
  369. }
  370. ],
  371. async (err, song) => {
  372. if (err) {
  373. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  374. this.log("ERROR", "SONGS_UPDATE", `Failed to update song "${songId}". "${err}"`);
  375. return cb({ status: "error", message: err });
  376. }
  377. this.log("SUCCESS", "SONGS_UPDATE", `Successfully updated song "${songId}".`);
  378. return cb({
  379. status: "success",
  380. message: "Song has been successfully updated",
  381. data: { song }
  382. });
  383. }
  384. );
  385. }),
  386. /**
  387. * Removes a song
  388. *
  389. * @param session
  390. * @param songId - the song id
  391. * @param cb
  392. */
  393. remove: isAdminRequired(async function remove(session, songId, cb) {
  394. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  395. const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
  396. async.waterfall(
  397. [
  398. next => {
  399. songModel.findOne({ _id: songId }, next);
  400. },
  401. (song, next) => {
  402. PlaylistsModule.runJob("GET_PLAYLISTS_WITH_SONG", { songId }, this)
  403. .then(res => {
  404. async.eachLimit(
  405. res.playlists,
  406. 1,
  407. (playlist, next) => {
  408. WSModule.runJob(
  409. "RUN_ACTION2",
  410. {
  411. session,
  412. namespace: "playlists",
  413. action: "removeSongFromPlaylist",
  414. args: [song.youtubeId, playlist._id]
  415. },
  416. this
  417. )
  418. .then(res => {
  419. if (res.status === "error") next(res.message);
  420. else next();
  421. })
  422. .catch(err => {
  423. next(err);
  424. });
  425. },
  426. err => {
  427. if (err) next(err);
  428. else next(null, song);
  429. }
  430. );
  431. })
  432. .catch(err => next(err));
  433. },
  434. (song, next) => {
  435. stationModel.find({ "queue._id": songId }, (err, stations) => {
  436. if (err) next(err);
  437. else {
  438. async.eachLimit(
  439. stations,
  440. 1,
  441. (station, next) => {
  442. WSModule.runJob(
  443. "RUN_ACTION2",
  444. {
  445. session,
  446. namespace: "stations",
  447. action: "removeFromQueue",
  448. args: [station._id, song.youtubeId]
  449. },
  450. this
  451. )
  452. .then(res => {
  453. if (
  454. res.status === "error" &&
  455. res.message !== "Station not found" &&
  456. res.message !== "Song is not currently in the queue."
  457. )
  458. next(res.message);
  459. else next();
  460. })
  461. .catch(err => {
  462. next(err);
  463. });
  464. },
  465. err => {
  466. if (err) next(err);
  467. else next();
  468. }
  469. );
  470. }
  471. });
  472. },
  473. next => {
  474. stationModel.find({ "currentSong._id": songId }, (err, stations) => {
  475. if (err) next(err);
  476. else {
  477. async.eachLimit(
  478. stations,
  479. 1,
  480. (station, next) => {
  481. StationsModule.runJob(
  482. "SKIP_STATION",
  483. { stationId: station._id, natural: false },
  484. this
  485. )
  486. .then(() => {
  487. next();
  488. })
  489. .catch(err => {
  490. if (err.message === "Station not found.") next();
  491. else next(err);
  492. });
  493. },
  494. err => {
  495. if (err) next(err);
  496. else next();
  497. }
  498. );
  499. }
  500. });
  501. },
  502. next => {
  503. songModel.deleteOne({ _id: songId }, err => {
  504. if (err) next(err);
  505. else next();
  506. });
  507. },
  508. next => {
  509. CacheModule.runJob("HDEL", { table: "songs", key: songId }, this)
  510. .then(() => {
  511. next();
  512. })
  513. .catch(next);
  514. }
  515. ],
  516. async err => {
  517. if (err) {
  518. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  519. this.log("ERROR", "SONGS_REMOVE", `Failed to remove song "${songId}". "${err}"`);
  520. return cb({ status: "error", message: err });
  521. }
  522. this.log("SUCCESS", "SONGS_REMOVE", `Successfully removed song "${songId}".`);
  523. CacheModule.runJob("PUB", {
  524. channel: "song.removed",
  525. value: { songId }
  526. });
  527. return cb({
  528. status: "success",
  529. message: "Song has been successfully removed"
  530. });
  531. }
  532. );
  533. }),
  534. /**
  535. * Removes many songs
  536. *
  537. * @param session
  538. * @param songIds - array of song ids
  539. * @param cb
  540. */
  541. removeMany: isAdminRequired(async function remove(session, songIds, cb) {
  542. const successful = [];
  543. const failed = [];
  544. async.waterfall(
  545. [
  546. next => {
  547. async.eachLimit(
  548. songIds,
  549. 1,
  550. (songId, next) => {
  551. WSModule.runJob(
  552. "RUN_ACTION2",
  553. {
  554. session,
  555. namespace: "songs",
  556. action: "remove",
  557. args: [songId]
  558. },
  559. this
  560. )
  561. .then(res => {
  562. if (res.status === "error") failed.push(songId);
  563. else successful.push(songId);
  564. next();
  565. })
  566. .catch(err => {
  567. next(err);
  568. });
  569. },
  570. err => {
  571. if (err) next(err);
  572. else next();
  573. }
  574. );
  575. }
  576. ],
  577. async err => {
  578. if (err) {
  579. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  580. this.log("ERROR", "SONGS_REMOVE_MANY", `Failed to remove songs "${failed.join(", ")}". "${err}"`);
  581. return cb({ status: "error", message: err });
  582. }
  583. let message = "";
  584. if (successful.length === 1) message += `1 song has been successfully removed`;
  585. else message += `${successful.length} songs have been successfully removed`;
  586. if (failed.length > 0) {
  587. this.log("ERROR", "SONGS_REMOVE_MANY", `Failed to remove songs "${failed.join(", ")}". "${err}"`);
  588. if (failed.length === 1) message += `, failed to remove 1 song`;
  589. else message += `, failed to remove ${failed.length} songs`;
  590. }
  591. this.log("SUCCESS", "SONGS_REMOVE_MANY", `${message} "${successful.join(", ")}"`);
  592. return cb({
  593. status: "success",
  594. message
  595. });
  596. }
  597. );
  598. }),
  599. /**
  600. * Searches through official songs
  601. *
  602. * @param {object} session - the session object automatically added by the websocket
  603. * @param {string} query - the query
  604. * @param {string} page - the page
  605. * @param {Function} cb - gets called with the result
  606. */
  607. searchOfficial: isLoginRequired(async function searchOfficial(session, query, page, cb) {
  608. async.waterfall(
  609. [
  610. next => {
  611. if ((!query && query !== "") || typeof query !== "string") next("Invalid query.");
  612. else next();
  613. },
  614. next => {
  615. SongsModule.runJob("SEARCH", {
  616. query,
  617. includeVerified: true,
  618. trimmed: true,
  619. page
  620. })
  621. .then(response => {
  622. next(null, response);
  623. })
  624. .catch(err => {
  625. next(err);
  626. });
  627. }
  628. ],
  629. async (err, data) => {
  630. if (err) {
  631. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  632. this.log("ERROR", "SONGS_SEARCH_OFFICIAL", `Searching songs failed. "${err}"`);
  633. return cb({ status: "error", message: err });
  634. }
  635. this.log("SUCCESS", "SONGS_SEARCH_OFFICIAL", "Searching songs successful.");
  636. return cb({ status: "success", data });
  637. }
  638. );
  639. }),
  640. /**
  641. * Verifies a song
  642. *
  643. * @param session
  644. * @param songId - the song id
  645. * @param cb
  646. */
  647. verify: isAdminRequired(async function add(session, songId, cb) {
  648. const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  649. async.waterfall(
  650. [
  651. next => {
  652. SongModel.findOne({ _id: songId }, next);
  653. },
  654. (song, next) => {
  655. if (!song) return next("This song is not in the database.");
  656. return next(null, song);
  657. },
  658. (song, next) => {
  659. const oldStatus = false;
  660. song.verifiedBy = session.userId;
  661. song.verifiedAt = Date.now();
  662. song.verified = true;
  663. song.save(err => next(err, song, oldStatus));
  664. },
  665. (song, oldStatus, next) => {
  666. song.genres.forEach(genre => {
  667. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre, createPlaylist: true })
  668. .then(() => {})
  669. .catch(() => {});
  670. });
  671. SongsModule.runJob("UPDATE_SONG", { songId: song._id, oldStatus });
  672. next(null, song, oldStatus);
  673. }
  674. ],
  675. async err => {
  676. if (err) {
  677. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  678. this.log("ERROR", "SONGS_VERIFY", `User "${session.userId}" failed to verify song. "${err}"`);
  679. return cb({ status: "error", message: err });
  680. }
  681. this.log("SUCCESS", "SONGS_VERIFY", `User "${session.userId}" successfully verified song "${songId}".`);
  682. return cb({
  683. status: "success",
  684. message: "Song has been verified successfully."
  685. });
  686. }
  687. );
  688. // TODO Check if video is in queue and Add the song to the appropriate stations
  689. }),
  690. /**
  691. * Verify many songs
  692. *
  693. * @param session
  694. * @param songIds - array of song ids
  695. * @param cb
  696. */
  697. verifyMany: isAdminRequired(async function verifyMany(session, songIds, cb) {
  698. const successful = [];
  699. const failed = [];
  700. async.waterfall(
  701. [
  702. next => {
  703. async.eachLimit(
  704. songIds,
  705. 1,
  706. (songId, next) => {
  707. WSModule.runJob(
  708. "RUN_ACTION2",
  709. {
  710. session,
  711. namespace: "songs",
  712. action: "verify",
  713. args: [songId]
  714. },
  715. this
  716. )
  717. .then(res => {
  718. if (res.status === "error") failed.push(songId);
  719. else successful.push(songId);
  720. next();
  721. })
  722. .catch(err => {
  723. next(err);
  724. });
  725. },
  726. err => {
  727. if (err) next(err);
  728. else next();
  729. }
  730. );
  731. }
  732. ],
  733. async err => {
  734. if (err) {
  735. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  736. this.log("ERROR", "SONGS_VERIFY_MANY", `Failed to verify songs "${failed.join(", ")}". "${err}"`);
  737. return cb({ status: "error", message: err });
  738. }
  739. let message = "";
  740. if (successful.length === 1) message += `1 song has been successfully verified`;
  741. else message += `${successful.length} songs have been successfully verified`;
  742. if (failed.length > 0) {
  743. this.log("ERROR", "SONGS_VERIFY_MANY", `Failed to verify songs "${failed.join(", ")}". "${err}"`);
  744. if (failed.length === 1) message += `, failed to verify 1 song`;
  745. else message += `, failed to verify ${failed.length} songs`;
  746. }
  747. this.log("SUCCESS", "SONGS_VERIFY_MANY", `${message} "${successful.join(", ")}"`);
  748. return cb({
  749. status: "success",
  750. message
  751. });
  752. }
  753. );
  754. }),
  755. /**
  756. * Un-verifies a song
  757. *
  758. * @param session
  759. * @param songId - the song id
  760. * @param cb
  761. */
  762. unverify: isAdminRequired(async function add(session, songId, cb) {
  763. const SongModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  764. async.waterfall(
  765. [
  766. next => {
  767. SongModel.findOne({ _id: songId }, next);
  768. },
  769. (song, next) => {
  770. if (!song) return next("This song is not in the database.");
  771. return next(null, song);
  772. },
  773. (song, next) => {
  774. song.verified = false;
  775. song.verifiedBy = null;
  776. song.verifiedAt = null;
  777. song.save(err => {
  778. next(err, song);
  779. });
  780. },
  781. (song, next) => {
  782. song.genres.forEach(genre => {
  783. PlaylistsModule.runJob("AUTOFILL_GENRE_PLAYLIST", { genre, createPlaylist: false })
  784. .then(() => {})
  785. .catch(() => {});
  786. });
  787. SongsModule.runJob("UPDATE_SONG", { songId, oldStatus: true });
  788. next(null);
  789. }
  790. ],
  791. async err => {
  792. if (err) {
  793. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  794. this.log("ERROR", "SONGS_UNVERIFY", `User "${session.userId}" failed to verify song. "${err}"`);
  795. return cb({ status: "error", message: err });
  796. }
  797. this.log(
  798. "SUCCESS",
  799. "SONGS_UNVERIFY",
  800. `User "${session.userId}" successfully unverified song "${songId}".`
  801. );
  802. return cb({
  803. status: "success",
  804. message: "Song has been unverified successfully."
  805. });
  806. }
  807. );
  808. // TODO Check if video is in queue and Add the song to the appropriate stations
  809. }),
  810. /**
  811. * Unverify many songs
  812. *
  813. * @param session
  814. * @param songIds - array of song ids
  815. * @param cb
  816. */
  817. unverifyMany: isAdminRequired(async function unverifyMany(session, songIds, cb) {
  818. const successful = [];
  819. const failed = [];
  820. async.waterfall(
  821. [
  822. next => {
  823. async.eachLimit(
  824. songIds,
  825. 1,
  826. (songId, next) => {
  827. WSModule.runJob(
  828. "RUN_ACTION2",
  829. {
  830. session,
  831. namespace: "songs",
  832. action: "unverify",
  833. args: [songId]
  834. },
  835. this
  836. )
  837. .then(res => {
  838. if (res.status === "error") failed.push(songId);
  839. else successful.push(songId);
  840. next();
  841. })
  842. .catch(err => {
  843. next(err);
  844. });
  845. },
  846. err => {
  847. if (err) next(err);
  848. else next();
  849. }
  850. );
  851. }
  852. ],
  853. async err => {
  854. if (err) {
  855. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  856. this.log(
  857. "ERROR",
  858. "SONGS_UNVERIFY_MANY",
  859. `Failed to unverify songs "${failed.join(", ")}". "${err}"`
  860. );
  861. return cb({ status: "error", message: err });
  862. }
  863. let message = "";
  864. if (successful.length === 1) message += `1 song has been successfully unverified`;
  865. else message += `${successful.length} songs have been successfully unverified`;
  866. if (failed.length > 0) {
  867. this.log(
  868. "ERROR",
  869. "SONGS_UNVERIFY_MANY",
  870. `Failed to unverify songs "${failed.join(", ")}". "${err}"`
  871. );
  872. if (failed.length === 1) message += `, failed to unverify 1 song`;
  873. else message += `, failed to unverify ${failed.length} songs`;
  874. }
  875. this.log("SUCCESS", "SONGS_UNVERIFY_MANY", `${message} "${successful.join(", ")}"`);
  876. return cb({
  877. status: "success",
  878. message
  879. });
  880. }
  881. );
  882. }),
  883. /**
  884. * Gets a list of all genres
  885. *
  886. * @param session
  887. * @param cb
  888. */
  889. getGenres: isAdminRequired(function getGenres(session, cb) {
  890. async.waterfall(
  891. [
  892. next => {
  893. SongsModule.runJob("GET_GENRES", this)
  894. .then(res => {
  895. next(null, res.genres);
  896. })
  897. .catch(next);
  898. }
  899. ],
  900. async (err, genres) => {
  901. if (err && err !== true) {
  902. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  903. this.log("ERROR", "GET_GENRES", `User ${session.userId} failed to get genres. '${err}'`);
  904. cb({ status: "error", message: err });
  905. } else {
  906. this.log("SUCCESS", "GET_GENRES", `User ${session.userId} has successfully got the genres.`);
  907. cb({
  908. status: "success",
  909. message: "Successfully got genres.",
  910. data: {
  911. items: genres
  912. }
  913. });
  914. }
  915. }
  916. );
  917. }),
  918. /**
  919. * Bulk update genres for selected songs
  920. *
  921. * @param session
  922. * @param method Whether to add, remove or replace genres
  923. * @param genres Array of genres to apply
  924. * @param songIds Array of songIds to apply genres to
  925. * @param cb
  926. */
  927. editGenres: isAdminRequired(async function editGenres(session, method, genres, songIds, cb) {
  928. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  929. async.waterfall(
  930. [
  931. next => {
  932. songModel.find({ _id: { $in: songIds } }, next);
  933. },
  934. (songs, next) => {
  935. const songsFound = songs.map(song => song._id);
  936. if (songsFound.length > 0) next(null, songsFound);
  937. else next("None of the specified songs were found.");
  938. },
  939. (songsFound, next) => {
  940. const query = {};
  941. if (method === "add") {
  942. query.$addToSet = { genres: { $each: genres } };
  943. } else if (method === "remove") {
  944. query.$pullAll = { genres };
  945. } else if (method === "replace") {
  946. query.$set = { genres };
  947. } else {
  948. next("Invalid method.");
  949. return;
  950. }
  951. songModel.updateMany({ _id: { $in: songsFound } }, query, { runValidators: true }, err => {
  952. if (err) {
  953. next(err);
  954. return;
  955. }
  956. SongsModule.runJob("UPDATE_SONGS", { songIds: songsFound });
  957. next();
  958. });
  959. }
  960. ],
  961. async err => {
  962. if (err && err !== true) {
  963. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  964. this.log("ERROR", "EDIT_GENRES", `User ${session.userId} failed to edit genres. '${err}'`);
  965. cb({ status: "error", message: err });
  966. } else {
  967. this.log("SUCCESS", "EDIT_GENRES", `User ${session.userId} has successfully edited genres.`);
  968. cb({
  969. status: "success",
  970. message: "Successfully edited genres."
  971. });
  972. }
  973. }
  974. );
  975. }),
  976. /**
  977. * Gets a list of all artists
  978. *
  979. * @param session
  980. * @param cb
  981. */
  982. getArtists: isAdminRequired(function getArtists(session, cb) {
  983. async.waterfall(
  984. [
  985. next => {
  986. SongsModule.runJob("GET_ARTISTS", this)
  987. .then(res => {
  988. next(null, res.artists);
  989. })
  990. .catch(next);
  991. }
  992. ],
  993. async (err, artists) => {
  994. if (err && err !== true) {
  995. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  996. this.log("ERROR", "GET_ARTISTS", `User ${session.userId} failed to get artists. '${err}'`);
  997. cb({ status: "error", message: err });
  998. } else {
  999. this.log("SUCCESS", "GET_ARTISTS", `User ${session.userId} has successfully got the artists.`);
  1000. cb({
  1001. status: "success",
  1002. message: "Successfully got artists.",
  1003. data: {
  1004. items: artists
  1005. }
  1006. });
  1007. }
  1008. }
  1009. );
  1010. }),
  1011. /**
  1012. * Bulk update artists for selected songs
  1013. *
  1014. * @param session
  1015. * @param method Whether to add, remove or replace artists
  1016. * @param artists Array of artists to apply
  1017. * @param songIds Array of songIds to apply artists to
  1018. * @param cb
  1019. */
  1020. editArtists: isAdminRequired(async function editArtists(session, method, artists, songIds, cb) {
  1021. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1022. async.waterfall(
  1023. [
  1024. next => {
  1025. songModel.find({ _id: { $in: songIds } }, next);
  1026. },
  1027. (songs, next) => {
  1028. const songsFound = songs.map(song => song._id);
  1029. if (songsFound.length > 0) next(null, songsFound);
  1030. else next("None of the specified songs were found.");
  1031. },
  1032. (songsFound, next) => {
  1033. const query = {};
  1034. if (method === "add") {
  1035. query.$addToSet = { artists: { $each: artists } };
  1036. } else if (method === "remove") {
  1037. query.$pullAll = { artists };
  1038. } else if (method === "replace") {
  1039. query.$set = { artists };
  1040. } else {
  1041. next("Invalid method.");
  1042. return;
  1043. }
  1044. songModel.updateMany({ _id: { $in: songsFound } }, query, { runValidators: true }, err => {
  1045. if (err) {
  1046. next(err);
  1047. return;
  1048. }
  1049. SongsModule.runJob("UPDATE_SONGS", { songIds: songsFound });
  1050. next();
  1051. });
  1052. }
  1053. ],
  1054. async err => {
  1055. if (err && err !== true) {
  1056. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1057. this.log("ERROR", "EDIT_ARTISTS", `User ${session.userId} failed to edit artists. '${err}'`);
  1058. cb({ status: "error", message: err });
  1059. } else {
  1060. this.log("SUCCESS", "EDIT_ARTISTS", `User ${session.userId} has successfully edited artists.`);
  1061. cb({
  1062. status: "success",
  1063. message: "Successfully edited artists."
  1064. });
  1065. }
  1066. }
  1067. );
  1068. }),
  1069. /**
  1070. * Gets a list of all tags
  1071. *
  1072. * @param session
  1073. * @param cb
  1074. */
  1075. getTags: isAdminRequired(function getTags(session, cb) {
  1076. async.waterfall(
  1077. [
  1078. next => {
  1079. SongsModule.runJob("GET_TAGS", this)
  1080. .then(res => {
  1081. next(null, res.tags);
  1082. })
  1083. .catch(next);
  1084. }
  1085. ],
  1086. async (err, tags) => {
  1087. if (err && err !== true) {
  1088. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1089. this.log("ERROR", "GET_TAGS", `User ${session.userId} failed to get tags. '${err}'`);
  1090. cb({ status: "error", message: err });
  1091. } else {
  1092. this.log("SUCCESS", "GET_TAGS", `User ${session.userId} has successfully got the tags.`);
  1093. cb({
  1094. status: "success",
  1095. message: "Successfully got tags.",
  1096. data: {
  1097. items: tags
  1098. }
  1099. });
  1100. }
  1101. }
  1102. );
  1103. }),
  1104. /**
  1105. * Bulk update tags for selected songs
  1106. *
  1107. * @param session
  1108. * @param method Whether to add, remove or replace tags
  1109. * @param tags Array of tags to apply
  1110. * @param songIds Array of songIds to apply tags to
  1111. * @param cb
  1112. */
  1113. editTags: isAdminRequired(async function editTags(session, method, tags, songIds, cb) {
  1114. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  1115. async.waterfall(
  1116. [
  1117. next => {
  1118. songModel.find({ _id: { $in: songIds } }, next);
  1119. },
  1120. (songs, next) => {
  1121. const songsFound = songs.map(song => song._id);
  1122. if (songsFound.length > 0) next(null, songsFound);
  1123. else next("None of the specified songs were found.");
  1124. },
  1125. (songsFound, next) => {
  1126. const query = {};
  1127. if (method === "add") {
  1128. query.$addToSet = { tags: { $each: tags } };
  1129. } else if (method === "remove") {
  1130. query.$pullAll = { tags };
  1131. } else if (method === "replace") {
  1132. query.$set = { tags };
  1133. } else {
  1134. next("Invalid method.");
  1135. return;
  1136. }
  1137. songModel.updateMany({ _id: { $in: songsFound } }, query, { runValidators: true }, err => {
  1138. if (err) {
  1139. next(err);
  1140. return;
  1141. }
  1142. SongsModule.runJob("UPDATE_SONGS", { songIds: songsFound });
  1143. next();
  1144. });
  1145. }
  1146. ],
  1147. async err => {
  1148. if (err && err !== true) {
  1149. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1150. this.log("ERROR", "EDIT_TAGS", `User ${session.userId} failed to edit tags. '${err}'`);
  1151. cb({ status: "error", message: err });
  1152. } else {
  1153. this.log("SUCCESS", "EDIT_TAGS", `User ${session.userId} has successfully edited tags.`);
  1154. cb({
  1155. status: "success",
  1156. message: "Successfully edited tags."
  1157. });
  1158. }
  1159. }
  1160. );
  1161. })
  1162. };