playlists.js 29 KB

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