playlists.js 29 KB

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