playlists.js 29 KB

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