playlists.js 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178
  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. export default {
  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. IOModule.runJob(
  602. "RUN_ACTION2",
  603. {
  604. session,
  605. namespace: "playlists",
  606. action: "addSongToPlaylist",
  607. args: [true, songIds[s], playlistId]
  608. },
  609. this
  610. )
  611. // eslint-disable-next-line no-loop-func
  612. .then(res => {
  613. if (res.status === "success") {
  614. addedSongs.push(songIds[s]);
  615. songsSuccess += 1;
  616. } else songsFail += 1;
  617. })
  618. // eslint-disable-next-line no-loop-func
  619. .catch(() => {
  620. songsFail += 1;
  621. })
  622. // eslint-disable-next-line no-loop-func
  623. .finally(() => {
  624. processed += 1;
  625. checkDone();
  626. });
  627. }
  628. },
  629. next => {
  630. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  631. .then(playlist => {
  632. next(null, playlist);
  633. })
  634. .catch(next);
  635. },
  636. (playlist, next) => {
  637. if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found.");
  638. return next(null, playlist);
  639. }
  640. ],
  641. async (err, playlist) => {
  642. if (err) {
  643. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  644. this.log(
  645. "ERROR",
  646. "PLAYLIST_IMPORT",
  647. `Importing a YouTube playlist to private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  648. );
  649. return cb({ status: "failure", message: err });
  650. }
  651. ActivitiesModule.runJob("ADD_ACTIVITY", {
  652. userId: session.userId,
  653. activityType: "added_songs_to_playlist",
  654. payload: addedSongs
  655. });
  656. this.log(
  657. "SUCCESS",
  658. "PLAYLIST_IMPORT",
  659. `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}.`
  660. );
  661. return cb({
  662. status: "success",
  663. message: "Playlist has been successfully imported.",
  664. data: playlist.songs,
  665. stats: {
  666. videosInPlaylistTotal,
  667. songsInPlaylistTotal,
  668. songsAddedSuccessfully: songsSuccess,
  669. songsFailedToAdd: songsFail
  670. }
  671. });
  672. }
  673. );
  674. }),
  675. /**
  676. * Removes a song from a private playlist
  677. *
  678. * @param {object} session - the session object automatically added by socket.io
  679. * @param {string} songId - the id of the song we are removing from the private playlist
  680. * @param {string} playlistId - the id of the playlist we are removing the song from
  681. * @param {Function} cb - gets called with the result
  682. */
  683. removeSongFromPlaylist: isLoginRequired(async function removeSongFromPlaylist(session, songId, playlistId, cb) {
  684. const playlistModel = await DBModule.runJob(
  685. "GET_MODEL",
  686. {
  687. modelName: "playlist"
  688. },
  689. this
  690. );
  691. async.waterfall(
  692. [
  693. next => {
  694. if (!songId || typeof songId !== "string") return next("Invalid song id.");
  695. if (!playlistId || typeof playlistId !== "string") return next("Invalid playlist id.");
  696. return next();
  697. },
  698. next => {
  699. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  700. .then(playlist => {
  701. next(null, playlist);
  702. })
  703. .catch(next);
  704. },
  705. (playlist, next) => {
  706. if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found");
  707. return playlistModel.updateOne({ _id: playlistId }, { $pull: { songs: { songId } } }, next);
  708. },
  709. (res, next) => {
  710. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  711. .then(playlist => {
  712. next(null, playlist);
  713. })
  714. .catch(next);
  715. }
  716. ],
  717. async (err, playlist) => {
  718. if (err) {
  719. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  720. this.log(
  721. "ERROR",
  722. "PLAYLIST_REMOVE_SONG",
  723. `Removing song "${songId}" from private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  724. );
  725. return cb({ status: "failure", message: err });
  726. }
  727. this.log(
  728. "SUCCESS",
  729. "PLAYLIST_REMOVE_SONG",
  730. `Successfully removed song "${songId}" from private playlist "${playlistId}" for user "${session.userId}".`
  731. );
  732. CacheModule.runJob("PUB", {
  733. channel: "playlist.removeSong",
  734. value: {
  735. playlistId: playlist._id,
  736. songId,
  737. userId: session.userId
  738. }
  739. });
  740. return cb({
  741. status: "success",
  742. message: "Song has been successfully removed from playlist",
  743. data: playlist.songs
  744. });
  745. }
  746. );
  747. }),
  748. /**
  749. * Updates the displayName of a private playlist
  750. *
  751. * @param {object} session - the session object automatically added by socket.io
  752. * @param {string} playlistId - the id of the playlist we are updating the displayName for
  753. * @param {Function} cb - gets called with the result
  754. */
  755. updateDisplayName: isLoginRequired(async function updateDisplayName(session, playlistId, displayName, cb) {
  756. const playlistModel = await DBModule.runJob(
  757. "GET_MODEL",
  758. {
  759. modelName: "playlist"
  760. },
  761. this
  762. );
  763. async.waterfall(
  764. [
  765. next => {
  766. playlistModel.updateOne(
  767. { _id: playlistId, createdBy: session.userId },
  768. { $set: { displayName } },
  769. { runValidators: true },
  770. next
  771. );
  772. },
  773. (res, next) => {
  774. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  775. .then(playlist => {
  776. next(null, playlist);
  777. })
  778. .catch(next);
  779. }
  780. ],
  781. async err => {
  782. if (err) {
  783. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  784. this.log(
  785. "ERROR",
  786. "PLAYLIST_UPDATE_DISPLAY_NAME",
  787. `Updating display name to "${displayName}" for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  788. );
  789. return cb({ status: "failure", message: err });
  790. }
  791. this.log(
  792. "SUCCESS",
  793. "PLAYLIST_UPDATE_DISPLAY_NAME",
  794. `Successfully updated display name to "${displayName}" for private playlist "${playlistId}" for user "${session.userId}".`
  795. );
  796. CacheModule.runJob("PUB", {
  797. channel: "playlist.updateDisplayName",
  798. value: {
  799. playlistId,
  800. displayName,
  801. userId: session.userId
  802. }
  803. });
  804. return cb({
  805. status: "success",
  806. message: "Playlist has been successfully updated"
  807. });
  808. }
  809. );
  810. }),
  811. /**
  812. * Moves a song to the top of the list in a private playlist
  813. *
  814. * @param {object} session - the session object automatically added by socket.io
  815. * @param {string} playlistId - the id of the playlist we are moving the song to the top from
  816. * @param {string} songId - the id of the song we are moving to the top of the list
  817. * @param {Function} cb - gets called with the result
  818. */
  819. moveSongToTop: isLoginRequired(async function moveSongToTop(session, playlistId, songId, cb) {
  820. const playlistModel = await DBModule.runJob(
  821. "GET_MODEL",
  822. {
  823. modelName: "playlist"
  824. },
  825. this
  826. );
  827. async.waterfall(
  828. [
  829. next => {
  830. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  831. .then(playlist => {
  832. next(null, playlist);
  833. })
  834. .catch(next);
  835. },
  836. (playlist, next) => {
  837. if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found");
  838. return async.each(
  839. playlist.songs,
  840. (song, next) => {
  841. if (song.songId === songId) return next(song);
  842. return next();
  843. },
  844. err => {
  845. if (err && err.songId) return next(null, err);
  846. return next("Song not found");
  847. }
  848. );
  849. },
  850. (song, next) => {
  851. playlistModel.updateOne({ _id: playlistId }, { $pull: { songs: { songId } } }, err => {
  852. if (err) return next(err);
  853. return next(null, song);
  854. });
  855. },
  856. (song, next) => {
  857. playlistModel.updateOne(
  858. { _id: playlistId },
  859. {
  860. $push: {
  861. songs: {
  862. $each: [song],
  863. $position: 0
  864. }
  865. }
  866. },
  867. next
  868. );
  869. },
  870. (res, next) => {
  871. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  872. .then(playlist => {
  873. next(null, playlist);
  874. })
  875. .catch(next);
  876. }
  877. ],
  878. async err => {
  879. if (err) {
  880. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  881. this.log(
  882. "ERROR",
  883. "PLAYLIST_MOVE_SONG_TO_TOP",
  884. `Moving song "${songId}" to the top for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  885. );
  886. return cb({ status: "failure", message: err });
  887. }
  888. this.log(
  889. "SUCCESS",
  890. "PLAYLIST_MOVE_SONG_TO_TOP",
  891. `Successfully moved song "${songId}" to the top for private playlist "${playlistId}" for user "${session.userId}".`
  892. );
  893. CacheModule.runJob("PUB", {
  894. channel: "playlist.moveSongToTop",
  895. value: {
  896. playlistId,
  897. songId,
  898. userId: session.userId
  899. }
  900. });
  901. return cb({
  902. status: "success",
  903. message: "Playlist has been successfully updated"
  904. });
  905. }
  906. );
  907. }),
  908. /**
  909. * Moves a song to the bottom of the list in a private playlist
  910. *
  911. * @param {object} session - the session object automatically added by socket.io
  912. * @param {string} playlistId - the id of the playlist we are moving the song to the bottom from
  913. * @param {string} songId - the id of the song we are moving to the bottom of the list
  914. * @param {Function} cb - gets called with the result
  915. */
  916. moveSongToBottom: isLoginRequired(async function moveSongToBottom(session, playlistId, songId, cb) {
  917. const playlistModel = await DBModule.runJob(
  918. "GET_MODEL",
  919. {
  920. modelName: "playlist"
  921. },
  922. this
  923. );
  924. async.waterfall(
  925. [
  926. next => {
  927. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  928. .then(playlist => {
  929. next(null, playlist);
  930. })
  931. .catch(next);
  932. },
  933. (playlist, next) => {
  934. if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found");
  935. return async.each(
  936. playlist.songs,
  937. (song, next) => {
  938. if (song.songId === songId) return next(song);
  939. return next();
  940. },
  941. err => {
  942. if (err && err.songId) return next(null, err);
  943. return next("Song not found");
  944. }
  945. );
  946. },
  947. (song, next) => {
  948. playlistModel.updateOne({ _id: playlistId }, { $pull: { songs: { songId } } }, err => {
  949. if (err) return next(err);
  950. return next(null, song);
  951. });
  952. },
  953. (song, next) => {
  954. playlistModel.updateOne(
  955. { _id: playlistId },
  956. {
  957. $push: {
  958. songs: song
  959. }
  960. },
  961. next
  962. );
  963. },
  964. (res, next) => {
  965. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  966. .then(playlist => {
  967. next(null, playlist);
  968. })
  969. .catch(next);
  970. }
  971. ],
  972. async err => {
  973. if (err) {
  974. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  975. this.log(
  976. "ERROR",
  977. "PLAYLIST_MOVE_SONG_TO_BOTTOM",
  978. `Moving song "${songId}" to the bottom for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  979. );
  980. return cb({ status: "failure", message: err });
  981. }
  982. this.log(
  983. "SUCCESS",
  984. "PLAYLIST_MOVE_SONG_TO_BOTTOM",
  985. `Successfully moved song "${songId}" to the bottom for private playlist "${playlistId}" for user "${session.userId}".`
  986. );
  987. CacheModule.runJob("PUB", {
  988. channel: "playlist.moveSongToBottom",
  989. value: {
  990. playlistId,
  991. songId,
  992. userId: session.userId
  993. }
  994. });
  995. return cb({
  996. status: "success",
  997. message: "Playlist has been successfully updated"
  998. });
  999. }
  1000. );
  1001. }),
  1002. /**
  1003. * Removes a private playlist
  1004. *
  1005. * @param {object} session - the session object automatically added by socket.io
  1006. * @param {string} playlistId - the id of the playlist we are moving the song to the top from
  1007. * @param {Function} cb - gets called with the result
  1008. */
  1009. remove: isLoginRequired(async function remove(session, playlistId, cb) {
  1010. const stationModel = await DBModule.runJob(
  1011. "GET_MODEL",
  1012. {
  1013. modelName: "station"
  1014. },
  1015. this
  1016. );
  1017. async.waterfall(
  1018. [
  1019. next => {
  1020. PlaylistsModule.runJob("DELETE_PLAYLIST", { playlistId }, this).then(next).catch(next);
  1021. },
  1022. next => {
  1023. stationModel.find({ privatePlaylist: playlistId }, (err, res) => {
  1024. next(err, res);
  1025. });
  1026. },
  1027. (stations, next) => {
  1028. async.each(
  1029. stations,
  1030. (station, next) => {
  1031. async.waterfall(
  1032. [
  1033. next => {
  1034. stationModel.updateOne(
  1035. { _id: station._id },
  1036. { $set: { privatePlaylist: null } },
  1037. { runValidators: true },
  1038. next
  1039. );
  1040. },
  1041. (res, next) => {
  1042. if (!station.partyMode) {
  1043. moduleManager.modules.stations
  1044. .runJob(
  1045. "UPDATE_STATION",
  1046. {
  1047. stationId: station._id
  1048. },
  1049. this
  1050. )
  1051. .then(station => next(null, station))
  1052. .catch(next);
  1053. CacheModule.runJob("PUB", {
  1054. channel: "privatePlaylist.selected",
  1055. value: {
  1056. playlistId: null,
  1057. stationId: station._id
  1058. }
  1059. });
  1060. } else next();
  1061. }
  1062. ],
  1063. () => {
  1064. next();
  1065. }
  1066. );
  1067. },
  1068. () => {
  1069. next();
  1070. }
  1071. );
  1072. }
  1073. ],
  1074. async err => {
  1075. if (err) {
  1076. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1077. this.log(
  1078. "ERROR",
  1079. "PLAYLIST_REMOVE",
  1080. `Removing private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  1081. );
  1082. return cb({ status: "failure", message: err });
  1083. }
  1084. this.log(
  1085. "SUCCESS",
  1086. "PLAYLIST_REMOVE",
  1087. `Successfully removed private playlist "${playlistId}" for user "${session.userId}".`
  1088. );
  1089. CacheModule.runJob("PUB", {
  1090. channel: "playlist.delete",
  1091. value: {
  1092. userId: session.userId,
  1093. playlistId
  1094. }
  1095. });
  1096. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1097. userId: session.userId,
  1098. activityType: "deleted_playlist",
  1099. payload: [playlistId]
  1100. });
  1101. return cb({
  1102. status: "success",
  1103. message: "Playlist successfully removed"
  1104. });
  1105. }
  1106. );
  1107. })
  1108. };