songs.js 32 KB

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