playlists.js 33 KB

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