playlists.js 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163
  1. import async from "async";
  2. import { isLoginRequired } from "./hooks";
  3. import moduleManager from "../../index";
  4. const DBModule = moduleManager.modules.db;
  5. const UtilsModule = moduleManager.modules.utils;
  6. const IOModule = moduleManager.modules.io;
  7. const SongsModule = moduleManager.modules.songs;
  8. const CacheModule = moduleManager.modules.cache;
  9. const PlaylistsModule = moduleManager.modules.playlists;
  10. const YouTubeModule = moduleManager.modules.youtube;
  11. const ActivitiesModule = moduleManager.modules.activities;
  12. CacheModule.runJob("SUB", {
  13. channel: "playlist.create",
  14. cb: playlist => {
  15. IOModule.runJob("SOCKETS_FROM_USER", { userId: playlist.createdBy }, this).then(response => {
  16. response.sockets.forEach(socket => {
  17. socket.emit("event:playlist.create", playlist);
  18. });
  19. });
  20. }
  21. });
  22. CacheModule.runJob("SUB", {
  23. channel: "playlist.delete",
  24. cb: res => {
  25. IOModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(response => {
  26. response.sockets.forEach(socket => {
  27. socket.emit("event:playlist.delete", res.playlistId);
  28. });
  29. });
  30. }
  31. });
  32. CacheModule.runJob("SUB", {
  33. channel: "playlist.moveSongToTop",
  34. cb: res => {
  35. IOModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(response => {
  36. response.sockets.forEach(socket => {
  37. socket.emit("event:playlist.moveSongToTop", {
  38. playlistId: res.playlistId,
  39. songId: res.songId
  40. });
  41. });
  42. });
  43. }
  44. });
  45. CacheModule.runJob("SUB", {
  46. channel: "playlist.moveSongToBottom",
  47. cb: res => {
  48. IOModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(response => {
  49. response.sockets.forEach(socket => {
  50. socket.emit("event:playlist.moveSongToBottom", {
  51. playlistId: res.playlistId,
  52. songId: res.songId
  53. });
  54. });
  55. });
  56. }
  57. });
  58. CacheModule.runJob("SUB", {
  59. channel: "playlist.addSong",
  60. cb: res => {
  61. IOModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(response => {
  62. response.sockets.forEach(socket => {
  63. socket.emit("event:playlist.addSong", {
  64. playlistId: res.playlistId,
  65. song: res.song
  66. });
  67. });
  68. });
  69. }
  70. });
  71. CacheModule.runJob("SUB", {
  72. channel: "playlist.removeSong",
  73. cb: res => {
  74. IOModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(response => {
  75. response.sockets.forEach(socket => {
  76. socket.emit("event:playlist.removeSong", {
  77. playlistId: res.playlistId,
  78. songId: res.songId
  79. });
  80. });
  81. });
  82. }
  83. });
  84. CacheModule.runJob("SUB", {
  85. channel: "playlist.updateDisplayName",
  86. cb: res => {
  87. IOModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(response => {
  88. response.sockets.forEach(socket => {
  89. socket.emit("event:playlist.updateDisplayName", {
  90. playlistId: res.playlistId,
  91. displayName: res.displayName
  92. });
  93. });
  94. });
  95. }
  96. });
  97. const lib = {
  98. /**
  99. * Gets the first song from a private playlist
  100. *
  101. * @param {object} session - the session object automatically added by socket.io
  102. * @param {string} playlistId - the id of the playlist we are getting the first song from
  103. * @param {Function} cb - gets called with the result
  104. */
  105. getFirstSong: isLoginRequired(function getFirstSong(session, playlistId, cb) {
  106. async.waterfall(
  107. [
  108. next => {
  109. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  110. .then(playlist => {
  111. next(null, playlist);
  112. })
  113. .catch(next);
  114. },
  115. (playlist, next) => {
  116. if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found.");
  117. return next(null, playlist.songs[0]);
  118. }
  119. ],
  120. async (err, song) => {
  121. if (err) {
  122. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  123. this.log(
  124. "ERROR",
  125. "PLAYLIST_GET_FIRST_SONG",
  126. `Getting the first song of playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  127. );
  128. return cb({ status: "failure", message: err });
  129. }
  130. this.log(
  131. "SUCCESS",
  132. "PLAYLIST_GET_FIRST_SONG",
  133. `Successfully got the first song of playlist "${playlistId}" for user "${session.userId}".`
  134. );
  135. return cb({
  136. status: "success",
  137. song
  138. });
  139. }
  140. );
  141. }),
  142. /**
  143. * Gets all playlists for the user requesting it
  144. *
  145. * @param {object} session - the session object automatically added by socket.io
  146. * @param {Function} cb - gets called with the result
  147. */
  148. indexForUser: isLoginRequired(async function indexForUser(session, cb) {
  149. const playlistModel = await DBModule.runJob(
  150. "GET_MODEL",
  151. {
  152. modelName: "playlist"
  153. },
  154. this
  155. );
  156. async.waterfall(
  157. [
  158. next => {
  159. playlistModel.find({ createdBy: session.userId }, next);
  160. }
  161. ],
  162. async (err, playlists) => {
  163. if (err) {
  164. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  165. this.log(
  166. "ERROR",
  167. "PLAYLIST_INDEX_FOR_USER",
  168. `Indexing playlists for user "${session.userId}" failed. "${err}"`
  169. );
  170. return cb({ status: "failure", message: err });
  171. }
  172. this.log(
  173. "SUCCESS",
  174. "PLAYLIST_INDEX_FOR_USER",
  175. `Successfully indexed playlists for user "${session.userId}".`
  176. );
  177. return cb({
  178. status: "success",
  179. data: playlists
  180. });
  181. }
  182. );
  183. }),
  184. /**
  185. * Creates a new private playlist
  186. *
  187. * @param {object} session - the session object automatically added by socket.io
  188. * @param {object} data - the data for the new private playlist
  189. * @param {Function} cb - gets called with the result
  190. */
  191. create: isLoginRequired(async function create(session, data, cb) {
  192. const playlistModel = await DBModule.runJob(
  193. "GET_MODEL",
  194. {
  195. modelName: "playlist"
  196. },
  197. this
  198. );
  199. async.waterfall(
  200. [
  201. next => (data ? next() : cb({ status: "failure", message: "Invalid data" })),
  202. next => {
  203. const { displayName, songs } = data;
  204. playlistModel.create(
  205. {
  206. displayName,
  207. songs,
  208. createdBy: session.userId,
  209. createdAt: Date.now()
  210. },
  211. next
  212. );
  213. }
  214. ],
  215. async (err, playlist) => {
  216. if (err) {
  217. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  218. this.log(
  219. "ERROR",
  220. "PLAYLIST_CREATE",
  221. `Creating private playlist failed for user "${session.userId}". "${err}"`
  222. );
  223. return cb({ status: "failure", message: err });
  224. }
  225. CacheModule.runJob("PUB", {
  226. channel: "playlist.create",
  227. value: playlist
  228. });
  229. ActivitiesModule.runJob("ADD_ACTIVITY", {
  230. userId: session.userId,
  231. activityType: "created_playlist",
  232. payload: [playlist._id]
  233. });
  234. this.log(
  235. "SUCCESS",
  236. "PLAYLIST_CREATE",
  237. `Successfully created private playlist for user "${session.userId}".`
  238. );
  239. return cb({
  240. status: "success",
  241. message: "Successfully created playlist",
  242. data: {
  243. _id: playlist._id
  244. }
  245. });
  246. }
  247. );
  248. }),
  249. /**
  250. * Gets a playlist from id
  251. *
  252. * @param {object} session - the session object automatically added by socket.io
  253. * @param {string} playlistId - the id of the playlist we are getting
  254. * @param {Function} cb - gets called with the result
  255. */
  256. getPlaylist: isLoginRequired(function getPlaylist(session, playlistId, cb) {
  257. async.waterfall(
  258. [
  259. next => {
  260. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  261. .then(playlist => {
  262. next(null, playlist);
  263. })
  264. .catch(next);
  265. },
  266. (playlist, next) => {
  267. if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found");
  268. return next(null, playlist);
  269. }
  270. ],
  271. async (err, playlist) => {
  272. if (err) {
  273. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  274. this.log(
  275. "ERROR",
  276. "PLAYLIST_GET",
  277. `Getting private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  278. );
  279. return cb({ status: "failure", message: err });
  280. }
  281. this.log(
  282. "SUCCESS",
  283. "PLAYLIST_GET",
  284. `Successfully got private playlist "${playlistId}" for user "${session.userId}".`
  285. );
  286. return cb({
  287. status: "success",
  288. data: playlist
  289. });
  290. }
  291. );
  292. }),
  293. /**
  294. * Obtains basic metadata of a playlist in order to format an activity
  295. *
  296. * @param {object} session - the session object automatically added by socket.io
  297. * @param {string} playlistId - the playlist id
  298. * @param {Function} cb - callback
  299. */
  300. getPlaylistForActivity(session, playlistId, cb) {
  301. async.waterfall(
  302. [
  303. next => {
  304. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  305. .then(playlist => {
  306. next(null, playlist);
  307. })
  308. .catch(next);
  309. }
  310. ],
  311. async (err, playlist) => {
  312. if (err) {
  313. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  314. this.log(
  315. "ERROR",
  316. "PLAYLISTS_GET_PLAYLIST_FOR_ACTIVITY",
  317. `Failed to obtain metadata of playlist ${playlistId} for activity formatting. "${err}"`
  318. );
  319. return cb({ status: "failure", message: err });
  320. }
  321. this.log(
  322. "SUCCESS",
  323. "PLAYLISTS_GET_PLAYLIST_FOR_ACTIVITY",
  324. `Obtained metadata of playlist ${playlistId} for activity formatting successfully.`
  325. );
  326. return cb({
  327. status: "success",
  328. data: {
  329. title: playlist.displayName
  330. }
  331. });
  332. }
  333. );
  334. },
  335. // TODO Remove this
  336. /**
  337. * Updates a private playlist
  338. *
  339. * @param {object} session - the session object automatically added by socket.io
  340. * @param {string} playlistId - the id of the playlist we are updating
  341. * @param {object} playlist - the new private playlist object
  342. * @param {Function} cb - gets called with the result
  343. */
  344. update: isLoginRequired(async function update(session, playlistId, playlist, cb) {
  345. const playlistModel = await DBModule.runJob(
  346. "GET_MODEL",
  347. {
  348. modelName: "playlist"
  349. },
  350. this
  351. );
  352. async.waterfall(
  353. [
  354. next => {
  355. playlistModel.updateOne(
  356. { _id: playlistId, createdBy: session.userId },
  357. playlist,
  358. { runValidators: true },
  359. next
  360. );
  361. },
  362. (res, next) => {
  363. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  364. .then(playlist => {
  365. next(null, playlist);
  366. })
  367. .catch(next);
  368. }
  369. ],
  370. async (err, playlist) => {
  371. if (err) {
  372. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  373. this.log(
  374. "ERROR",
  375. "PLAYLIST_UPDATE",
  376. `Updating private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  377. );
  378. return cb({ status: "failure", message: err });
  379. }
  380. this.log(
  381. "SUCCESS",
  382. "PLAYLIST_UPDATE",
  383. `Successfully updated private playlist "${playlistId}" for user "${session.userId}".`
  384. );
  385. return cb({
  386. status: "success",
  387. data: playlist
  388. });
  389. }
  390. );
  391. }),
  392. /**
  393. * Updates a private playlist
  394. *
  395. * @param {object} session - the session object automatically added by socket.io
  396. * @param {string} playlistId - the id of the playlist we are updating
  397. * @param {Function} cb - gets called with the result
  398. */
  399. shuffle: isLoginRequired(async function shuffle(session, playlistId, cb) {
  400. const playlistModel = await DBModule.runJob(
  401. "GET_MODEL",
  402. {
  403. modelName: "playlist"
  404. },
  405. this
  406. );
  407. async.waterfall(
  408. [
  409. next => {
  410. if (!playlistId) return next("No playlist id.");
  411. return playlistModel.findById(playlistId, next);
  412. },
  413. (playlist, next) => {
  414. UtilsModule.runJob("SHUFFLE", { array: playlist.songs }, this)
  415. .then(result => {
  416. next(null, result.array);
  417. })
  418. .catch(next);
  419. },
  420. (songs, next) => {
  421. playlistModel.updateOne({ _id: playlistId }, { $set: { songs } }, { runValidators: true }, next);
  422. },
  423. (res, next) => {
  424. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  425. .then(playlist => {
  426. next(null, playlist);
  427. })
  428. .catch(next);
  429. }
  430. ],
  431. async (err, playlist) => {
  432. if (err) {
  433. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  434. this.log(
  435. "ERROR",
  436. "PLAYLIST_SHUFFLE",
  437. `Updating private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  438. );
  439. return cb({ status: "failure", message: err });
  440. }
  441. this.log(
  442. "SUCCESS",
  443. "PLAYLIST_SHUFFLE",
  444. `Successfully updated private playlist "${playlistId}" for user "${session.userId}".`
  445. );
  446. return cb({
  447. status: "success",
  448. message: "Successfully shuffled playlist.",
  449. data: playlist
  450. });
  451. }
  452. );
  453. }),
  454. /**
  455. * Adds a song to a private playlist
  456. *
  457. * @param {object} session - the session object automatically added by socket.io
  458. * @param {boolean} isSet - is the song part of a set of songs to be added
  459. * @param {string} songId - the id of the song we are trying to add
  460. * @param {string} playlistId - the id of the playlist we are adding the song to
  461. * @param {Function} cb - gets called with the result
  462. */
  463. addSongToPlaylist: isLoginRequired(async function addSongToPlaylist(session, isSet, songId, playlistId, cb) {
  464. const playlistModel = await DBModule.runJob(
  465. "GET_MODEL",
  466. {
  467. modelName: "playlist"
  468. },
  469. this
  470. );
  471. async.waterfall(
  472. [
  473. next => {
  474. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  475. .then(playlist => {
  476. if (!playlist || playlist.createdBy !== session.userId)
  477. return next("Something went wrong when trying to get the playlist");
  478. return async.each(
  479. playlist.songs,
  480. (song, next) => {
  481. if (song.songId === songId) return next("That song is already in the playlist");
  482. return next();
  483. },
  484. next
  485. );
  486. })
  487. .catch(next);
  488. },
  489. next => {
  490. SongsModule.runJob("GET_SONG", { id: songId }, this)
  491. .then(response => {
  492. const { song } = response;
  493. next(null, {
  494. _id: song._id,
  495. songId,
  496. title: song.title,
  497. duration: song.duration
  498. });
  499. })
  500. .catch(() => {
  501. YouTubeModule.runJob("GET_SONG", { songId }, this)
  502. .then(response => next(null, response.song))
  503. .catch(next);
  504. });
  505. },
  506. (newSong, next) => {
  507. playlistModel.updateOne(
  508. { _id: playlistId },
  509. { $push: { songs: newSong } },
  510. { runValidators: true },
  511. err => {
  512. if (err) return next(err);
  513. return PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  514. .then(playlist => next(null, playlist, newSong))
  515. .catch(next);
  516. }
  517. );
  518. }
  519. ],
  520. async (err, playlist, newSong) => {
  521. if (err) {
  522. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  523. this.log(
  524. "ERROR",
  525. "PLAYLIST_ADD_SONG",
  526. `Adding song "${songId}" to private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  527. );
  528. return cb({ status: "failure", message: err });
  529. }
  530. this.log(
  531. "SUCCESS",
  532. "PLAYLIST_ADD_SONG",
  533. `Successfully added song "${songId}" to private playlist "${playlistId}" for user "${session.userId}".`
  534. );
  535. if (!isSet)
  536. ActivitiesModule.runJob("ADD_ACTIVITY", {
  537. userId: session.userId,
  538. activityType: "added_song_to_playlist",
  539. payload: [{ songId, playlistId }]
  540. });
  541. CacheModule.runJob("PUB", {
  542. channel: "playlist.addSong",
  543. value: {
  544. playlistId: playlist._id,
  545. song: newSong,
  546. userId: session.userId
  547. }
  548. });
  549. return cb({
  550. status: "success",
  551. message: "Song has been successfully added to the playlist",
  552. data: playlist.songs
  553. });
  554. }
  555. );
  556. }),
  557. /**
  558. * Adds a set of songs to a private playlist
  559. *
  560. * @param {object} session - the session object automatically added by socket.io
  561. * @param {string} url - the url of the the YouTube playlist
  562. * @param {string} playlistId - the id of the playlist we are adding the set of songs to
  563. * @param {boolean} musicOnly - whether to only add music to the playlist
  564. * @param {Function} cb - gets called with the result
  565. */
  566. addSetToPlaylist: isLoginRequired(function addSetToPlaylist(session, url, playlistId, musicOnly, cb) {
  567. let videosInPlaylistTotal = 0;
  568. let songsInPlaylistTotal = 0;
  569. let songsSuccess = 0;
  570. let songsFail = 0;
  571. const addedSongs = [];
  572. async.waterfall(
  573. [
  574. next => {
  575. YouTubeModule.runJob(
  576. "GET_PLAYLIST",
  577. {
  578. url,
  579. musicOnly
  580. },
  581. this
  582. ).then(response => {
  583. if (response.filteredSongs) {
  584. videosInPlaylistTotal = response.songs.length;
  585. songsInPlaylistTotal = response.filteredSongs.length;
  586. } else {
  587. songsInPlaylistTotal = videosInPlaylistTotal = response.songs.length;
  588. }
  589. next(null, response.songs);
  590. });
  591. },
  592. (songIds, next) => {
  593. let processed = 0;
  594. /**
  595. *
  596. */
  597. function checkDone() {
  598. if (processed === songIds.length) next();
  599. }
  600. for (let s = 0; s < songIds.length; s += 1) {
  601. // eslint-disable-next-line no-loop-func
  602. lib.addSongToPlaylist(session, true, songIds[s], playlistId, res => {
  603. processed += 1;
  604. if (res.status === "success") {
  605. addedSongs.push(songIds[s]);
  606. songsSuccess += 1;
  607. } else songsFail += 1;
  608. checkDone();
  609. });
  610. }
  611. },
  612. next => {
  613. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  614. .then(playlist => {
  615. next(null, playlist);
  616. })
  617. .catch(next);
  618. },
  619. (playlist, next) => {
  620. if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found.");
  621. return next(null, playlist);
  622. }
  623. ],
  624. async (err, playlist) => {
  625. if (err) {
  626. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  627. this.log(
  628. "ERROR",
  629. "PLAYLIST_IMPORT",
  630. `Importing a YouTube playlist to private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  631. );
  632. return cb({ status: "failure", message: err });
  633. }
  634. ActivitiesModule.runJob("ADD_ACTIVITY", {
  635. userId: session.userId,
  636. activityType: "added_songs_to_playlist",
  637. payload: addedSongs
  638. });
  639. this.log(
  640. "SUCCESS",
  641. "PLAYLIST_IMPORT",
  642. `Successfully imported a YouTube playlist to private playlist "${playlistId}" for user "${session.userId}". Videos in playlist: ${videosInPlaylistTotal}, songs in playlist: ${songsInPlaylistTotal}, songs successfully added: ${songsSuccess}, songs failed: ${songsFail}.`
  643. );
  644. return cb({
  645. status: "success",
  646. message: "Playlist has been successfully imported.",
  647. data: playlist.songs,
  648. stats: {
  649. videosInPlaylistTotal,
  650. songsInPlaylistTotal,
  651. songsAddedSuccessfully: songsSuccess,
  652. songsFailedToAdd: songsFail
  653. }
  654. });
  655. }
  656. );
  657. }),
  658. /**
  659. * Removes a song from a private playlist
  660. *
  661. * @param {object} session - the session object automatically added by socket.io
  662. * @param {string} songId - the id of the song we are removing from the private playlist
  663. * @param {string} playlistId - the id of the playlist we are removing the song from
  664. * @param {Function} cb - gets called with the result
  665. */
  666. removeSongFromPlaylist: isLoginRequired(async function removeSongFromPlaylist(session, songId, playlistId, cb) {
  667. const playlistModel = await DBModule.runJob(
  668. "GET_MODEL",
  669. {
  670. modelName: "playlist"
  671. },
  672. this
  673. );
  674. async.waterfall(
  675. [
  676. next => {
  677. if (!songId || typeof songId !== "string") return next("Invalid song id.");
  678. if (!playlistId || typeof playlistId !== "string") return next("Invalid playlist id.");
  679. return next();
  680. },
  681. next => {
  682. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  683. .then(playlist => {
  684. next(null, playlist);
  685. })
  686. .catch(next);
  687. },
  688. (playlist, next) => {
  689. if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found");
  690. return playlistModel.updateOne({ _id: playlistId }, { $pull: { songs: { songId } } }, next);
  691. },
  692. (res, next) => {
  693. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  694. .then(playlist => {
  695. next(null, playlist);
  696. })
  697. .catch(next);
  698. }
  699. ],
  700. async (err, playlist) => {
  701. if (err) {
  702. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  703. this.log(
  704. "ERROR",
  705. "PLAYLIST_REMOVE_SONG",
  706. `Removing song "${songId}" from private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  707. );
  708. return cb({ status: "failure", message: err });
  709. }
  710. this.log(
  711. "SUCCESS",
  712. "PLAYLIST_REMOVE_SONG",
  713. `Successfully removed song "${songId}" from private playlist "${playlistId}" for user "${session.userId}".`
  714. );
  715. CacheModule.runJob("PUB", {
  716. channel: "playlist.removeSong",
  717. value: {
  718. playlistId: playlist._id,
  719. songId,
  720. userId: session.userId
  721. }
  722. });
  723. return cb({
  724. status: "success",
  725. message: "Song has been successfully removed from playlist",
  726. data: playlist.songs
  727. });
  728. }
  729. );
  730. }),
  731. /**
  732. * Updates the displayName of a private playlist
  733. *
  734. * @param {object} session - the session object automatically added by socket.io
  735. * @param {string} playlistId - the id of the playlist we are updating the displayName for
  736. * @param {Function} cb - gets called with the result
  737. */
  738. updateDisplayName: isLoginRequired(async function updateDisplayName(session, playlistId, displayName, cb) {
  739. const playlistModel = await DBModule.runJob(
  740. "GET_MODEL",
  741. {
  742. modelName: "playlist"
  743. },
  744. this
  745. );
  746. async.waterfall(
  747. [
  748. next => {
  749. playlistModel.updateOne(
  750. { _id: playlistId, createdBy: session.userId },
  751. { $set: { displayName } },
  752. { runValidators: true },
  753. next
  754. );
  755. },
  756. (res, next) => {
  757. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  758. .then(playlist => {
  759. next(null, playlist);
  760. })
  761. .catch(next);
  762. }
  763. ],
  764. async err => {
  765. if (err) {
  766. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  767. this.log(
  768. "ERROR",
  769. "PLAYLIST_UPDATE_DISPLAY_NAME",
  770. `Updating display name to "${displayName}" for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  771. );
  772. return cb({ status: "failure", message: err });
  773. }
  774. this.log(
  775. "SUCCESS",
  776. "PLAYLIST_UPDATE_DISPLAY_NAME",
  777. `Successfully updated display name to "${displayName}" for private playlist "${playlistId}" for user "${session.userId}".`
  778. );
  779. CacheModule.runJob("PUB", {
  780. channel: "playlist.updateDisplayName",
  781. value: {
  782. playlistId,
  783. displayName,
  784. userId: session.userId
  785. }
  786. });
  787. return cb({
  788. status: "success",
  789. message: "Playlist has been successfully updated"
  790. });
  791. }
  792. );
  793. }),
  794. /**
  795. * Moves a song to the top of the list in a private playlist
  796. *
  797. * @param {object} session - the session object automatically added by socket.io
  798. * @param {string} playlistId - the id of the playlist we are moving the song to the top from
  799. * @param {string} songId - the id of the song we are moving to the top of the list
  800. * @param {Function} cb - gets called with the result
  801. */
  802. moveSongToTop: isLoginRequired(async function moveSongToTop(session, playlistId, songId, cb) {
  803. const playlistModel = await DBModule.runJob(
  804. "GET_MODEL",
  805. {
  806. modelName: "playlist"
  807. },
  808. this
  809. );
  810. async.waterfall(
  811. [
  812. next => {
  813. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  814. .then(playlist => {
  815. next(null, playlist);
  816. })
  817. .catch(next);
  818. },
  819. (playlist, next) => {
  820. if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found");
  821. return async.each(
  822. playlist.songs,
  823. (song, next) => {
  824. if (song.songId === songId) return next(song);
  825. return next();
  826. },
  827. err => {
  828. if (err && err.songId) return next(null, err);
  829. return next("Song not found");
  830. }
  831. );
  832. },
  833. (song, next) => {
  834. playlistModel.updateOne({ _id: playlistId }, { $pull: { songs: { songId } } }, err => {
  835. if (err) return next(err);
  836. return next(null, song);
  837. });
  838. },
  839. (song, next) => {
  840. playlistModel.updateOne(
  841. { _id: playlistId },
  842. {
  843. $push: {
  844. songs: {
  845. $each: [song],
  846. $position: 0
  847. }
  848. }
  849. },
  850. next
  851. );
  852. },
  853. (res, next) => {
  854. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  855. .then(playlist => {
  856. next(null, playlist);
  857. })
  858. .catch(next);
  859. }
  860. ],
  861. async err => {
  862. if (err) {
  863. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  864. this.log(
  865. "ERROR",
  866. "PLAYLIST_MOVE_SONG_TO_TOP",
  867. `Moving song "${songId}" to the top for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  868. );
  869. return cb({ status: "failure", message: err });
  870. }
  871. this.log(
  872. "SUCCESS",
  873. "PLAYLIST_MOVE_SONG_TO_TOP",
  874. `Successfully moved song "${songId}" to the top for private playlist "${playlistId}" for user "${session.userId}".`
  875. );
  876. CacheModule.runJob("PUB", {
  877. channel: "playlist.moveSongToTop",
  878. value: {
  879. playlistId,
  880. songId,
  881. userId: session.userId
  882. }
  883. });
  884. return cb({
  885. status: "success",
  886. message: "Playlist has been successfully updated"
  887. });
  888. }
  889. );
  890. }),
  891. /**
  892. * Moves a song to the bottom of the list in a private playlist
  893. *
  894. * @param {object} session - the session object automatically added by socket.io
  895. * @param {string} playlistId - the id of the playlist we are moving the song to the bottom from
  896. * @param {string} songId - the id of the song we are moving to the bottom of the list
  897. * @param {Function} cb - gets called with the result
  898. */
  899. moveSongToBottom: isLoginRequired(async function moveSongToBottom(session, playlistId, songId, cb) {
  900. const playlistModel = await DBModule.runJob(
  901. "GET_MODEL",
  902. {
  903. modelName: "playlist"
  904. },
  905. this
  906. );
  907. async.waterfall(
  908. [
  909. next => {
  910. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  911. .then(playlist => {
  912. next(null, playlist);
  913. })
  914. .catch(next);
  915. },
  916. (playlist, next) => {
  917. if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found");
  918. return async.each(
  919. playlist.songs,
  920. (song, next) => {
  921. if (song.songId === songId) return next(song);
  922. return next();
  923. },
  924. err => {
  925. if (err && err.songId) return next(null, err);
  926. return next("Song not found");
  927. }
  928. );
  929. },
  930. (song, next) => {
  931. playlistModel.updateOne({ _id: playlistId }, { $pull: { songs: { songId } } }, err => {
  932. if (err) return next(err);
  933. return next(null, song);
  934. });
  935. },
  936. (song, next) => {
  937. playlistModel.updateOne(
  938. { _id: playlistId },
  939. {
  940. $push: {
  941. songs: song
  942. }
  943. },
  944. next
  945. );
  946. },
  947. (res, next) => {
  948. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  949. .then(playlist => {
  950. next(null, playlist);
  951. })
  952. .catch(next);
  953. }
  954. ],
  955. async err => {
  956. if (err) {
  957. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  958. this.log(
  959. "ERROR",
  960. "PLAYLIST_MOVE_SONG_TO_BOTTOM",
  961. `Moving song "${songId}" to the bottom for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  962. );
  963. return cb({ status: "failure", message: err });
  964. }
  965. this.log(
  966. "SUCCESS",
  967. "PLAYLIST_MOVE_SONG_TO_BOTTOM",
  968. `Successfully moved song "${songId}" to the bottom for private playlist "${playlistId}" for user "${session.userId}".`
  969. );
  970. CacheModule.runJob("PUB", {
  971. channel: "playlist.moveSongToBottom",
  972. value: {
  973. playlistId,
  974. songId,
  975. userId: session.userId
  976. }
  977. });
  978. return cb({
  979. status: "success",
  980. message: "Playlist has been successfully updated"
  981. });
  982. }
  983. );
  984. }),
  985. /**
  986. * Removes a private playlist
  987. *
  988. * @param {object} session - the session object automatically added by socket.io
  989. * @param {string} playlistId - the id of the playlist we are moving the song to the top from
  990. * @param {Function} cb - gets called with the result
  991. */
  992. remove: isLoginRequired(async function remove(session, playlistId, cb) {
  993. const stationModel = await DBModule.runJob(
  994. "GET_MODEL",
  995. {
  996. modelName: "station"
  997. },
  998. this
  999. );
  1000. async.waterfall(
  1001. [
  1002. next => {
  1003. PlaylistsModule.runJob("DELETE_PLAYLIST", { playlistId }, this).then(next).catch(next);
  1004. },
  1005. next => {
  1006. stationModel.find({ privatePlaylist: playlistId }, (err, res) => {
  1007. next(err, res);
  1008. });
  1009. },
  1010. (stations, next) => {
  1011. async.each(
  1012. stations,
  1013. (station, next) => {
  1014. async.waterfall(
  1015. [
  1016. next => {
  1017. stationModel.updateOne(
  1018. { _id: station._id },
  1019. { $set: { privatePlaylist: null } },
  1020. { runValidators: true },
  1021. next
  1022. );
  1023. },
  1024. (res, next) => {
  1025. if (!station.partyMode) {
  1026. moduleManager.modules.stations
  1027. .runJob(
  1028. "UPDATE_STATION",
  1029. {
  1030. stationId: station._id
  1031. },
  1032. this
  1033. )
  1034. .then(station => next(null, station))
  1035. .catch(next);
  1036. CacheModule.runJob("PUB", {
  1037. channel: "privatePlaylist.selected",
  1038. value: {
  1039. playlistId: null,
  1040. stationId: station._id
  1041. }
  1042. });
  1043. } else next();
  1044. }
  1045. ],
  1046. () => {
  1047. next();
  1048. }
  1049. );
  1050. },
  1051. () => {
  1052. next();
  1053. }
  1054. );
  1055. }
  1056. ],
  1057. async err => {
  1058. if (err) {
  1059. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1060. this.log(
  1061. "ERROR",
  1062. "PLAYLIST_REMOVE",
  1063. `Removing private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  1064. );
  1065. return cb({ status: "failure", message: err });
  1066. }
  1067. this.log(
  1068. "SUCCESS",
  1069. "PLAYLIST_REMOVE",
  1070. `Successfully removed private playlist "${playlistId}" for user "${session.userId}".`
  1071. );
  1072. CacheModule.runJob("PUB", {
  1073. channel: "playlist.delete",
  1074. value: {
  1075. userId: session.userId,
  1076. playlistId
  1077. }
  1078. });
  1079. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1080. userId: session.userId,
  1081. activityType: "deleted_playlist",
  1082. payload: [playlistId]
  1083. });
  1084. return cb({
  1085. status: "success",
  1086. message: "Playlist successfully removed"
  1087. });
  1088. }
  1089. );
  1090. })
  1091. };
  1092. export default lib;