playlists.js 30 KB

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