playlists.js 47 KB

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