playlists.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257
  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. UtilsModule.runJob("SHUFFLE", { array: playlist.songs }, this)
  428. .then(result => {
  429. next(null, result.array);
  430. })
  431. .catch(next);
  432. },
  433. (songs, next) => {
  434. playlistModel.updateOne({ _id: playlistId }, { $set: { songs } }, { runValidators: true }, next);
  435. },
  436. (res, next) => {
  437. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  438. .then(playlist => {
  439. next(null, playlist);
  440. })
  441. .catch(next);
  442. }
  443. ],
  444. async (err, playlist) => {
  445. if (err) {
  446. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  447. this.log(
  448. "ERROR",
  449. "PLAYLIST_SHUFFLE",
  450. `Updating private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  451. );
  452. return cb({ status: "failure", message: err });
  453. }
  454. this.log(
  455. "SUCCESS",
  456. "PLAYLIST_SHUFFLE",
  457. `Successfully updated private playlist "${playlistId}" for user "${session.userId}".`
  458. );
  459. return cb({
  460. status: "success",
  461. message: "Successfully shuffled playlist.",
  462. data: playlist
  463. });
  464. }
  465. );
  466. }),
  467. /**
  468. * Adds a song to a private playlist
  469. *
  470. * @param {object} session - the session object automatically added by socket.io
  471. * @param {boolean} isSet - is the song part of a set of songs to be added
  472. * @param {string} songId - the id of the song we are trying to add
  473. * @param {string} playlistId - the id of the playlist we are adding the song to
  474. * @param {Function} cb - gets called with the result
  475. */
  476. addSongToPlaylist: isLoginRequired(async function addSongToPlaylist(session, isSet, songId, playlistId, cb) {
  477. const playlistModel = await DBModule.runJob(
  478. "GET_MODEL",
  479. {
  480. modelName: "playlist"
  481. },
  482. this
  483. );
  484. async.waterfall(
  485. [
  486. next => {
  487. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  488. .then(playlist => {
  489. if (!playlist || playlist.createdBy !== session.userId)
  490. return next("Something went wrong when trying to get the playlist");
  491. return async.each(
  492. playlist.songs,
  493. (song, next) => {
  494. if (song.songId === songId) return next("That song is already in the playlist");
  495. return next();
  496. },
  497. next
  498. );
  499. })
  500. .catch(next);
  501. },
  502. next => {
  503. SongsModule.runJob("GET_SONG", { id: songId }, this)
  504. .then(response => {
  505. const { song } = response;
  506. next(null, {
  507. _id: song._id,
  508. songId,
  509. title: song.title,
  510. duration: song.duration
  511. });
  512. })
  513. .catch(() => {
  514. YouTubeModule.runJob("GET_SONG", { songId }, this)
  515. .then(response => next(null, response.song))
  516. .catch(next);
  517. });
  518. },
  519. (newSong, next) => {
  520. playlistModel.updateOne(
  521. { _id: playlistId },
  522. { $push: { songs: newSong } },
  523. { runValidators: true },
  524. err => {
  525. if (err) return next(err);
  526. return PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  527. .then(playlist => next(null, playlist, newSong))
  528. .catch(next);
  529. }
  530. );
  531. }
  532. ],
  533. async (err, playlist, newSong) => {
  534. if (err) {
  535. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  536. this.log(
  537. "ERROR",
  538. "PLAYLIST_ADD_SONG",
  539. `Adding song "${songId}" to private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  540. );
  541. return cb({ status: "failure", message: err });
  542. }
  543. this.log(
  544. "SUCCESS",
  545. "PLAYLIST_ADD_SONG",
  546. `Successfully added song "${songId}" to private playlist "${playlistId}" for user "${session.userId}".`
  547. );
  548. if (!isSet)
  549. ActivitiesModule.runJob("ADD_ACTIVITY", {
  550. userId: session.userId,
  551. activityType: "added_song_to_playlist",
  552. payload: [{ songId, playlistId }]
  553. });
  554. CacheModule.runJob("PUB", {
  555. channel: "playlist.addSong",
  556. value: {
  557. playlistId: playlist._id,
  558. song: newSong,
  559. userId: session.userId
  560. }
  561. });
  562. return cb({
  563. status: "success",
  564. message: "Song has been successfully added to the playlist",
  565. data: playlist.songs
  566. });
  567. }
  568. );
  569. }),
  570. /**
  571. * Adds a set of songs to a private playlist
  572. *
  573. * @param {object} session - the session object automatically added by socket.io
  574. * @param {string} url - the url of the the YouTube playlist
  575. * @param {string} playlistId - the id of the playlist we are adding the set of songs to
  576. * @param {boolean} musicOnly - whether to only add music to the playlist
  577. * @param {Function} cb - gets called with the result
  578. */
  579. addSetToPlaylist: isLoginRequired(function addSetToPlaylist(session, url, playlistId, musicOnly, cb) {
  580. let videosInPlaylistTotal = 0;
  581. let songsInPlaylistTotal = 0;
  582. let addSongsStats = null;
  583. const addedSongs = [];
  584. async.waterfall(
  585. [
  586. next => {
  587. YouTubeModule.runJob(
  588. "GET_PLAYLIST",
  589. {
  590. url,
  591. musicOnly
  592. },
  593. this
  594. ).then(response => {
  595. if (response.filteredSongs) {
  596. videosInPlaylistTotal = response.songs.length;
  597. songsInPlaylistTotal = response.filteredSongs.length;
  598. } else {
  599. songsInPlaylistTotal = videosInPlaylistTotal = response.songs.length;
  600. }
  601. next(null, response.songs);
  602. });
  603. },
  604. (songIds, next) => {
  605. let successful = 0;
  606. let failed = 0;
  607. let alreadyInPlaylist = 0;
  608. if (songIds.length === 0) next();
  609. async.eachLimit(
  610. songIds,
  611. 1,
  612. (songId, next) => {
  613. IOModule.runJob(
  614. "RUN_ACTION2",
  615. {
  616. session,
  617. namespace: "playlists",
  618. action: "addSongToPlaylist",
  619. args: [true, songId, playlistId]
  620. },
  621. this
  622. )
  623. .then(res => {
  624. if (res.status === "success") {
  625. successful += 1;
  626. addedSongs.push(songId);
  627. } else failed += 1;
  628. if (res.message === "That song is already in the playlist") alreadyInPlaylist += 1;
  629. })
  630. .catch(() => {
  631. failed += 1;
  632. })
  633. .finally(() => {
  634. next();
  635. });
  636. },
  637. () => {
  638. addSongsStats = { successful, failed, alreadyInPlaylist };
  639. next(null);
  640. }
  641. );
  642. },
  643. next => {
  644. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  645. .then(playlist => {
  646. next(null, playlist);
  647. })
  648. .catch(next);
  649. },
  650. (playlist, next) => {
  651. if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found.");
  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 || typeof playlistId !== "string") 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. playlistModel.updateOne(
  779. { _id: playlistId, createdBy: session.userId },
  780. { $set: { displayName } },
  781. { runValidators: true },
  782. next
  783. );
  784. },
  785. (res, next) => {
  786. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  787. .then(playlist => {
  788. next(null, playlist);
  789. })
  790. .catch(next);
  791. }
  792. ],
  793. async err => {
  794. if (err) {
  795. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  796. this.log(
  797. "ERROR",
  798. "PLAYLIST_UPDATE_DISPLAY_NAME",
  799. `Updating display name to "${displayName}" for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  800. );
  801. return cb({ status: "failure", message: err });
  802. }
  803. this.log(
  804. "SUCCESS",
  805. "PLAYLIST_UPDATE_DISPLAY_NAME",
  806. `Successfully updated display name to "${displayName}" for private playlist "${playlistId}" for user "${session.userId}".`
  807. );
  808. CacheModule.runJob("PUB", {
  809. channel: "playlist.updateDisplayName",
  810. value: {
  811. playlistId,
  812. displayName,
  813. userId: session.userId
  814. }
  815. });
  816. return cb({
  817. status: "success",
  818. message: "Playlist has been successfully updated"
  819. });
  820. }
  821. );
  822. }),
  823. /**
  824. * Moves a song to the top of the list in a private playlist
  825. *
  826. * @param {object} session - the session object automatically added by socket.io
  827. * @param {string} playlistId - the id of the playlist we are moving the song to the top from
  828. * @param {string} songId - the id of the song we are moving to the top of the list
  829. * @param {Function} cb - gets called with the result
  830. */
  831. moveSongToTop: isLoginRequired(async function moveSongToTop(session, playlistId, songId, cb) {
  832. const playlistModel = await DBModule.runJob(
  833. "GET_MODEL",
  834. {
  835. modelName: "playlist"
  836. },
  837. this
  838. );
  839. async.waterfall(
  840. [
  841. next => {
  842. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  843. .then(playlist => {
  844. next(null, playlist);
  845. })
  846. .catch(next);
  847. },
  848. (playlist, next) => {
  849. if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found");
  850. return async.each(
  851. playlist.songs,
  852. (song, next) => {
  853. if (song.songId === songId) return next(song);
  854. return next();
  855. },
  856. err => {
  857. if (err && err.songId) return next(null, err);
  858. return next("Song not found");
  859. }
  860. );
  861. },
  862. (song, next) => {
  863. playlistModel.updateOne({ _id: playlistId }, { $pull: { songs: { songId } } }, err => {
  864. if (err) return next(err);
  865. return next(null, song);
  866. });
  867. },
  868. (song, next) => {
  869. playlistModel.updateOne(
  870. { _id: playlistId },
  871. {
  872. $push: {
  873. songs: {
  874. $each: [song],
  875. $position: 0
  876. }
  877. }
  878. },
  879. next
  880. );
  881. },
  882. (res, next) => {
  883. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  884. .then(playlist => {
  885. next(null, playlist);
  886. })
  887. .catch(next);
  888. }
  889. ],
  890. async err => {
  891. if (err) {
  892. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  893. this.log(
  894. "ERROR",
  895. "PLAYLIST_MOVE_SONG_TO_TOP",
  896. `Moving song "${songId}" to the top for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  897. );
  898. return cb({ status: "failure", message: err });
  899. }
  900. this.log(
  901. "SUCCESS",
  902. "PLAYLIST_MOVE_SONG_TO_TOP",
  903. `Successfully moved song "${songId}" to the top for private playlist "${playlistId}" for user "${session.userId}".`
  904. );
  905. CacheModule.runJob("PUB", {
  906. channel: "playlist.moveSongToTop",
  907. value: {
  908. playlistId,
  909. songId,
  910. userId: session.userId
  911. }
  912. });
  913. return cb({
  914. status: "success",
  915. message: "Playlist has been successfully updated"
  916. });
  917. }
  918. );
  919. }),
  920. /**
  921. * Moves a song to the bottom of the list in a private playlist
  922. *
  923. * @param {object} session - the session object automatically added by socket.io
  924. * @param {string} playlistId - the id of the playlist we are moving the song to the bottom from
  925. * @param {string} songId - the id of the song we are moving to the bottom of the list
  926. * @param {Function} cb - gets called with the result
  927. */
  928. moveSongToBottom: isLoginRequired(async function moveSongToBottom(session, playlistId, songId, cb) {
  929. const playlistModel = await DBModule.runJob(
  930. "GET_MODEL",
  931. {
  932. modelName: "playlist"
  933. },
  934. this
  935. );
  936. async.waterfall(
  937. [
  938. next => {
  939. PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
  940. .then(playlist => {
  941. next(null, playlist);
  942. })
  943. .catch(next);
  944. },
  945. (playlist, next) => {
  946. if (!playlist || playlist.createdBy !== session.userId) return next("Playlist not found");
  947. return async.each(
  948. playlist.songs,
  949. (song, next) => {
  950. if (song.songId === songId) return next(song);
  951. return next();
  952. },
  953. err => {
  954. if (err && err.songId) return next(null, err);
  955. return next("Song not found");
  956. }
  957. );
  958. },
  959. (song, next) => {
  960. playlistModel.updateOne({ _id: playlistId }, { $pull: { songs: { songId } } }, err => {
  961. if (err) return next(err);
  962. return next(null, song);
  963. });
  964. },
  965. (song, next) => {
  966. playlistModel.updateOne(
  967. { _id: playlistId },
  968. {
  969. $push: {
  970. songs: song
  971. }
  972. },
  973. next
  974. );
  975. },
  976. (res, next) => {
  977. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  978. .then(playlist => {
  979. next(null, playlist);
  980. })
  981. .catch(next);
  982. }
  983. ],
  984. async err => {
  985. if (err) {
  986. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  987. this.log(
  988. "ERROR",
  989. "PLAYLIST_MOVE_SONG_TO_BOTTOM",
  990. `Moving song "${songId}" to the bottom for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  991. );
  992. return cb({ status: "failure", message: err });
  993. }
  994. this.log(
  995. "SUCCESS",
  996. "PLAYLIST_MOVE_SONG_TO_BOTTOM",
  997. `Successfully moved song "${songId}" to the bottom for private playlist "${playlistId}" for user "${session.userId}".`
  998. );
  999. CacheModule.runJob("PUB", {
  1000. channel: "playlist.moveSongToBottom",
  1001. value: {
  1002. playlistId,
  1003. songId,
  1004. userId: session.userId
  1005. }
  1006. });
  1007. return cb({
  1008. status: "success",
  1009. message: "Playlist has been successfully updated"
  1010. });
  1011. }
  1012. );
  1013. }),
  1014. /**
  1015. * Removes a private playlist
  1016. *
  1017. * @param {object} session - the session object automatically added by socket.io
  1018. * @param {string} playlistId - the id of the playlist we are moving the song to the top from
  1019. * @param {Function} cb - gets called with the result
  1020. */
  1021. remove: isLoginRequired(async function remove(session, playlistId, cb) {
  1022. const stationModel = await DBModule.runJob(
  1023. "GET_MODEL",
  1024. {
  1025. modelName: "station"
  1026. },
  1027. this
  1028. );
  1029. async.waterfall(
  1030. [
  1031. next => {
  1032. PlaylistsModule.runJob("DELETE_PLAYLIST", { playlistId }, this).then(next).catch(next);
  1033. },
  1034. next => {
  1035. stationModel.find({ privatePlaylist: playlistId }, (err, res) => {
  1036. next(err, res);
  1037. });
  1038. },
  1039. (stations, next) => {
  1040. async.each(
  1041. stations,
  1042. (station, next) => {
  1043. async.waterfall(
  1044. [
  1045. next => {
  1046. stationModel.updateOne(
  1047. { _id: station._id },
  1048. { $set: { privatePlaylist: null } },
  1049. { runValidators: true },
  1050. next
  1051. );
  1052. },
  1053. (res, next) => {
  1054. if (!station.partyMode) {
  1055. moduleManager.modules.stations
  1056. .runJob(
  1057. "UPDATE_STATION",
  1058. {
  1059. stationId: station._id
  1060. },
  1061. this
  1062. )
  1063. .then(station => next(null, station))
  1064. .catch(next);
  1065. CacheModule.runJob("PUB", {
  1066. channel: "privatePlaylist.selected",
  1067. value: {
  1068. playlistId: null,
  1069. stationId: station._id
  1070. }
  1071. });
  1072. } else next();
  1073. }
  1074. ],
  1075. () => {
  1076. next();
  1077. }
  1078. );
  1079. },
  1080. () => {
  1081. next();
  1082. }
  1083. );
  1084. }
  1085. ],
  1086. async err => {
  1087. if (err) {
  1088. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1089. this.log(
  1090. "ERROR",
  1091. "PLAYLIST_REMOVE",
  1092. `Removing private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  1093. );
  1094. return cb({ status: "failure", message: err });
  1095. }
  1096. this.log(
  1097. "SUCCESS",
  1098. "PLAYLIST_REMOVE",
  1099. `Successfully removed private playlist "${playlistId}" for user "${session.userId}".`
  1100. );
  1101. CacheModule.runJob("PUB", {
  1102. channel: "playlist.delete",
  1103. value: {
  1104. userId: session.userId,
  1105. playlistId
  1106. }
  1107. });
  1108. ActivitiesModule.runJob("ADD_ACTIVITY", {
  1109. userId: session.userId,
  1110. activityType: "deleted_playlist",
  1111. payload: [playlistId]
  1112. });
  1113. return cb({
  1114. status: "success",
  1115. message: "Playlist successfully removed"
  1116. });
  1117. }
  1118. );
  1119. }),
  1120. /**
  1121. * Updates the privacy of a private playlist
  1122. *
  1123. * @param {object} session - the session object automatically added by socket.io
  1124. * @param {string} playlistId - the id of the playlist we are updating the displayName for
  1125. * @param {Function} cb - gets called with the result
  1126. */
  1127. updatePrivacy: isLoginRequired(async function updatePrivacy(session, playlistId, privacy, cb) {
  1128. const playlistModel = await DBModule.runJob(
  1129. "GET_MODEL",
  1130. {
  1131. modelName: "playlist"
  1132. },
  1133. this
  1134. );
  1135. async.waterfall(
  1136. [
  1137. next => {
  1138. playlistModel.updateOne(
  1139. { _id: playlistId, createdBy: session.userId },
  1140. { $set: { privacy } },
  1141. { runValidators: true },
  1142. next
  1143. );
  1144. },
  1145. (res, next) => {
  1146. PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
  1147. .then(playlist => {
  1148. next(null, playlist);
  1149. })
  1150. .catch(next);
  1151. }
  1152. ],
  1153. async err => {
  1154. if (err) {
  1155. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  1156. this.log(
  1157. "ERROR",
  1158. "PLAYLIST_UPDATE_PRIVACY",
  1159. `Updating privacy to "${privacy}" for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
  1160. );
  1161. return cb({ status: "failure", message: err });
  1162. }
  1163. this.log(
  1164. "SUCCESS",
  1165. "PLAYLIST_UPDATE_PRIVACY",
  1166. `Successfully updated privacy to "${privacy}" for private playlist "${playlistId}" for user "${session.userId}".`
  1167. );
  1168. CacheModule.runJob("PUB", {
  1169. channel: "playlist.updatePrivacy",
  1170. value: {
  1171. playlistId,
  1172. privacy,
  1173. userId: session.userId
  1174. }
  1175. });
  1176. return cb({
  1177. status: "success",
  1178. message: "Playlist has been successfully updated"
  1179. });
  1180. }
  1181. );
  1182. })
  1183. };