playlists.js 46 KB

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