playlists.js 46 KB

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