playlists.js 38 KB

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