playlists.js 30 KB

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