media.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806
  1. import async from "async";
  2. import { isAdminRequired, isLoginRequired } from "./hooks";
  3. // eslint-disable-next-line
  4. import moduleManager from "../../index";
  5. const DBModule = moduleManager.modules.db;
  6. const UtilsModule = moduleManager.modules.utils;
  7. const WSModule = moduleManager.modules.ws;
  8. const CacheModule = moduleManager.modules.cache;
  9. const SongsModule = moduleManager.modules.songs;
  10. const ActivitiesModule = moduleManager.modules.activities;
  11. const MediaModule = moduleManager.modules.media;
  12. CacheModule.runJob("SUB", {
  13. channel: "ratings.like",
  14. cb: data => {
  15. WSModule.runJob("EMIT_TO_ROOM", {
  16. room: `song.${data.youtubeId}`,
  17. args: [
  18. "event:ratings.liked",
  19. {
  20. data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes }
  21. }
  22. ]
  23. });
  24. WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
  25. sockets.forEach(socket => {
  26. socket.dispatch("event:ratings.updated", {
  27. data: {
  28. youtubeId: data.youtubeId,
  29. liked: true,
  30. disliked: false
  31. }
  32. });
  33. });
  34. });
  35. }
  36. });
  37. CacheModule.runJob("SUB", {
  38. channel: "ratings.dislike",
  39. cb: data => {
  40. WSModule.runJob("EMIT_TO_ROOM", {
  41. room: `song.${data.youtubeId}`,
  42. args: [
  43. "event:ratings.disliked",
  44. {
  45. data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes }
  46. }
  47. ]
  48. });
  49. WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
  50. sockets.forEach(socket => {
  51. socket.dispatch("event:ratings.updated", {
  52. data: {
  53. youtubeId: data.youtubeId,
  54. liked: false,
  55. disliked: true
  56. }
  57. });
  58. });
  59. });
  60. }
  61. });
  62. CacheModule.runJob("SUB", {
  63. channel: "ratings.unlike",
  64. cb: data => {
  65. WSModule.runJob("EMIT_TO_ROOM", {
  66. room: `song.${data.youtubeId}`,
  67. args: [
  68. "event:ratings.unliked",
  69. {
  70. data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes }
  71. }
  72. ]
  73. });
  74. WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
  75. sockets.forEach(socket => {
  76. socket.dispatch("event:ratings.updated", {
  77. data: {
  78. youtubeId: data.youtubeId,
  79. liked: false,
  80. disliked: false
  81. }
  82. });
  83. });
  84. });
  85. }
  86. });
  87. CacheModule.runJob("SUB", {
  88. channel: "ratings.undislike",
  89. cb: data => {
  90. WSModule.runJob("EMIT_TO_ROOM", {
  91. room: `song.${data.youtubeId}`,
  92. args: [
  93. "event:ratings.undisliked",
  94. {
  95. data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes }
  96. }
  97. ]
  98. });
  99. WSModule.runJob("SOCKETS_FROM_USER", { userId: data.userId }).then(sockets => {
  100. sockets.forEach(socket => {
  101. socket.dispatch("event:ratings.updated", {
  102. data: {
  103. youtubeId: data.youtubeId,
  104. liked: false,
  105. disliked: false
  106. }
  107. });
  108. });
  109. });
  110. }
  111. });
  112. export default {
  113. /**
  114. * Recalculates all ratings
  115. *
  116. * @param {object} session - the session object automatically added by the websocket
  117. * @param cb
  118. */
  119. recalculateAllRatings: isAdminRequired(async function recalculateAllRatings(session, cb) {
  120. async.waterfall(
  121. [
  122. next => {
  123. MediaModule.runJob("RECALCULATE_ALL_RATINGS", {}, this)
  124. .then(() => {
  125. next();
  126. })
  127. .catch(err => {
  128. next(err);
  129. });
  130. }
  131. ],
  132. async err => {
  133. if (err) {
  134. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  135. this.log("ERROR", "MEDIA_RECALCULATE_ALL_RATINGS", `Failed to recalculate all ratings. "${err}"`);
  136. return cb({ status: "error", message: err });
  137. }
  138. this.log("SUCCESS", "MEDIA_RECALCULATE_ALL_RATINGS", `Recalculated all ratings successfully.`);
  139. return cb({ status: "success", message: "Successfully recalculated all ratings." });
  140. }
  141. );
  142. }),
  143. /**
  144. * Like
  145. *
  146. * @param session
  147. * @param youtubeId - the youtube id
  148. * @param cb
  149. */
  150. like: isLoginRequired(async function like(session, youtubeId, cb) {
  151. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  152. async.waterfall(
  153. [
  154. next => {
  155. MediaModule.runJob(
  156. "GET_MEDIA",
  157. {
  158. youtubeId
  159. },
  160. this
  161. )
  162. .then(response => {
  163. const { song } = response;
  164. const { _id, title, artists, thumbnail, duration, verified } = song;
  165. next(null, {
  166. _id,
  167. youtubeId,
  168. title,
  169. artists,
  170. thumbnail,
  171. duration,
  172. verified
  173. });
  174. })
  175. .catch(next);
  176. },
  177. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  178. (song, user, next) => {
  179. if (!user) return next("User does not exist.");
  180. return this.module
  181. .runJob(
  182. "RUN_ACTION2",
  183. {
  184. session,
  185. namespace: "playlists",
  186. action: "removeSongFromPlaylist",
  187. args: [youtubeId, user.dislikedSongsPlaylist]
  188. },
  189. this
  190. )
  191. .then(() => next(null, song, user.likedSongsPlaylist))
  192. .catch(res => {
  193. if (!(res.message && res.message === "That song is not currently in the playlist."))
  194. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  195. return next(null, song, user.likedSongsPlaylist);
  196. });
  197. },
  198. (song, likedSongsPlaylist, next) =>
  199. this.module
  200. .runJob(
  201. "RUN_ACTION2",
  202. {
  203. session,
  204. namespace: "playlists",
  205. action: "addSongToPlaylist",
  206. args: [false, youtubeId, likedSongsPlaylist]
  207. },
  208. this
  209. )
  210. .then(() => next(null, song))
  211. .catch(res => {
  212. if (res.message && res.message === "That song is already in the playlist")
  213. return next("You have already liked this song.");
  214. return next("Unable to add song to the 'Liked Songs' playlist.");
  215. }),
  216. (song, next) => {
  217. MediaModule.runJob("RECALCULATE_RATINGS", { youtubeId })
  218. .then(ratings => next(null, song, ratings))
  219. .catch(err => next(err));
  220. }
  221. ],
  222. async (err, song, ratings) => {
  223. if (err) {
  224. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  225. this.log(
  226. "ERROR",
  227. "MEDIA_RATINGS_LIKE",
  228. `User "${session.userId}" failed to like song ${youtubeId}. "${err}"`
  229. );
  230. return cb({ status: "error", message: err });
  231. }
  232. const { likes, dislikes } = ratings;
  233. if (song._id) SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  234. CacheModule.runJob("PUB", {
  235. channel: "ratings.like",
  236. value: JSON.stringify({
  237. youtubeId,
  238. userId: session.userId,
  239. likes,
  240. dislikes
  241. })
  242. });
  243. ActivitiesModule.runJob("ADD_ACTIVITY", {
  244. userId: session.userId,
  245. type: "song__like",
  246. payload: {
  247. message: `Liked song <youtubeId>${song.title} by ${song.artists.join(", ")}</youtubeId>`,
  248. youtubeId,
  249. thumbnail: song.thumbnail
  250. }
  251. });
  252. return cb({
  253. status: "success",
  254. message: "You have successfully liked this song."
  255. });
  256. }
  257. );
  258. }),
  259. /**
  260. * Dislike
  261. *
  262. * @param session
  263. * @param youtubeId - the youtube id
  264. * @param cb
  265. */
  266. dislike: isLoginRequired(async function dislike(session, youtubeId, cb) {
  267. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  268. async.waterfall(
  269. [
  270. next => {
  271. MediaModule.runJob(
  272. "GET_MEDIA",
  273. {
  274. youtubeId
  275. },
  276. this
  277. )
  278. .then(response => {
  279. const { song } = response;
  280. const { _id, title, artists, thumbnail, duration, verified } = song;
  281. next(null, {
  282. _id,
  283. youtubeId,
  284. title,
  285. artists,
  286. thumbnail,
  287. duration,
  288. verified
  289. });
  290. })
  291. .catch(next);
  292. },
  293. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  294. (song, user, next) => {
  295. if (!user) return next("User does not exist.");
  296. return this.module
  297. .runJob(
  298. "RUN_ACTION2",
  299. {
  300. session,
  301. namespace: "playlists",
  302. action: "removeSongFromPlaylist",
  303. args: [youtubeId, user.likedSongsPlaylist]
  304. },
  305. this
  306. )
  307. .then(() => next(null, song, user.dislikedSongsPlaylist))
  308. .catch(res => {
  309. if (!(res.message && res.message === "That song is not currently in the playlist."))
  310. return next("Unable to remove song from the 'Liked Songs' playlist.");
  311. return next(null, song, user.dislikedSongsPlaylist);
  312. });
  313. },
  314. (song, dislikedSongsPlaylist, next) =>
  315. this.module
  316. .runJob(
  317. "RUN_ACTION2",
  318. {
  319. session,
  320. namespace: "playlists",
  321. action: "addSongToPlaylist",
  322. args: [false, youtubeId, dislikedSongsPlaylist]
  323. },
  324. this
  325. )
  326. .then(() => next(null, song))
  327. .catch(res => {
  328. if (res.message && res.message === "That song is already in the playlist")
  329. return next("You have already disliked this song.");
  330. return next("Unable to add song to the 'Disliked Songs' playlist.");
  331. }),
  332. (song, next) => {
  333. MediaModule.runJob("RECALCULATE_RATINGS", { youtubeId })
  334. .then(ratings => next(null, song, ratings))
  335. .catch(err => next(err));
  336. }
  337. ],
  338. async (err, song, ratings) => {
  339. if (err) {
  340. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  341. this.log(
  342. "ERROR",
  343. "MEDIA_RATINGS_DISLIKE",
  344. `User "${session.userId}" failed to dislike song ${youtubeId}. "${err}"`
  345. );
  346. return cb({ status: "error", message: err });
  347. }
  348. const { likes, dislikes } = ratings;
  349. if (song._id) SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  350. CacheModule.runJob("PUB", {
  351. channel: "ratings.dislike",
  352. value: JSON.stringify({
  353. youtubeId,
  354. userId: session.userId,
  355. likes,
  356. dislikes
  357. })
  358. });
  359. ActivitiesModule.runJob("ADD_ACTIVITY", {
  360. userId: session.userId,
  361. type: "song__dislike",
  362. payload: {
  363. message: `Disliked song <youtubeId>${song.title} by ${song.artists.join(", ")}</youtubeId>`,
  364. youtubeId,
  365. thumbnail: song.thumbnail
  366. }
  367. });
  368. return cb({
  369. status: "success",
  370. message: "You have successfully disliked this song."
  371. });
  372. }
  373. );
  374. }),
  375. /**
  376. * Undislike
  377. *
  378. * @param session
  379. * @param youtubeId - the youtube id
  380. * @param cb
  381. */
  382. undislike: isLoginRequired(async function undislike(session, youtubeId, cb) {
  383. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  384. async.waterfall(
  385. [
  386. next => {
  387. MediaModule.runJob(
  388. "GET_MEDIA",
  389. {
  390. youtubeId
  391. },
  392. this
  393. )
  394. .then(response => {
  395. const { song } = response;
  396. const { _id, title, artists, thumbnail, duration, verified } = song;
  397. next(null, {
  398. _id,
  399. youtubeId,
  400. title,
  401. artists,
  402. thumbnail,
  403. duration,
  404. verified
  405. });
  406. })
  407. .catch(next);
  408. },
  409. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  410. (song, user, next) => {
  411. if (!user) return next("User does not exist.");
  412. return this.module
  413. .runJob(
  414. "RUN_ACTION2",
  415. {
  416. session,
  417. namespace: "playlists",
  418. action: "removeSongFromPlaylist",
  419. args: [youtubeId, user.dislikedSongsPlaylist]
  420. },
  421. this
  422. )
  423. .then(res => {
  424. if (res.status === "error")
  425. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  426. return next(null, song, user.likedSongsPlaylist);
  427. })
  428. .catch(err => next(err));
  429. },
  430. (song, likedSongsPlaylist, next) => {
  431. this.module
  432. .runJob(
  433. "RUN_ACTION2",
  434. {
  435. session,
  436. namespace: "playlists",
  437. action: "removeSongFromPlaylist",
  438. args: [youtubeId, likedSongsPlaylist]
  439. },
  440. this
  441. )
  442. .then(() => next(null, song))
  443. .catch(res => {
  444. if (!(res.message && res.message === "That song is not currently in the playlist."))
  445. return next("Unable to remove song from the 'Liked Songs' playlist.");
  446. return next(null, song);
  447. });
  448. },
  449. (song, next) => {
  450. MediaModule.runJob("RECALCULATE_RATINGS", { youtubeId })
  451. .then(ratings => next(null, song, ratings))
  452. .catch(err => next(err));
  453. }
  454. ],
  455. async (err, song, ratings) => {
  456. if (err) {
  457. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  458. this.log(
  459. "ERROR",
  460. "MEDIA_RATINGS_UNDISLIKE",
  461. `User "${session.userId}" failed to undislike song ${youtubeId}. "${err}"`
  462. );
  463. return cb({ status: "error", message: err });
  464. }
  465. const { likes, dislikes } = ratings;
  466. if (song._id) SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  467. CacheModule.runJob("PUB", {
  468. channel: "ratings.undislike",
  469. value: JSON.stringify({
  470. youtubeId,
  471. userId: session.userId,
  472. likes,
  473. dislikes
  474. })
  475. });
  476. ActivitiesModule.runJob("ADD_ACTIVITY", {
  477. userId: session.userId,
  478. type: "song__undislike",
  479. payload: {
  480. message: `Removed <youtubeId>${song.title} by ${song.artists.join(
  481. ", "
  482. )}</youtubeId> from your Disliked Songs`,
  483. youtubeId,
  484. thumbnail: song.thumbnail
  485. }
  486. });
  487. return cb({
  488. status: "success",
  489. message: "You have successfully undisliked this song."
  490. });
  491. }
  492. );
  493. }),
  494. /**
  495. * Unlike
  496. *
  497. * @param session
  498. * @param youtubeId - the youtube id
  499. * @param cb
  500. */
  501. unlike: isLoginRequired(async function unlike(session, youtubeId, cb) {
  502. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  503. async.waterfall(
  504. [
  505. next => {
  506. MediaModule.runJob(
  507. "GET_MEDIA",
  508. {
  509. youtubeId
  510. },
  511. this
  512. )
  513. .then(response => {
  514. const { song } = response;
  515. const { _id, title, artists, thumbnail, duration, verified } = song;
  516. next(null, {
  517. _id,
  518. youtubeId,
  519. title,
  520. artists,
  521. thumbnail,
  522. duration,
  523. verified
  524. });
  525. })
  526. .catch(next);
  527. },
  528. (song, next) => userModel.findOne({ _id: session.userId }, (err, user) => next(err, song, user)),
  529. (song, user, next) => {
  530. if (!user) return next("User does not exist.");
  531. return this.module
  532. .runJob(
  533. "RUN_ACTION2",
  534. {
  535. session,
  536. namespace: "playlists",
  537. action: "removeSongFromPlaylist",
  538. args: [youtubeId, user.dislikedSongsPlaylist]
  539. },
  540. this
  541. )
  542. .then(() => next(null, song, user.likedSongsPlaylist))
  543. .catch(res => {
  544. if (!(res.message && res.message === "That song is not currently in the playlist."))
  545. return next("Unable to remove song from the 'Disliked Songs' playlist.");
  546. return next(null, song, user.likedSongsPlaylist);
  547. });
  548. },
  549. (song, likedSongsPlaylist, next) => {
  550. this.module
  551. .runJob(
  552. "RUN_ACTION2",
  553. {
  554. session,
  555. namespace: "playlists",
  556. action: "removeSongFromPlaylist",
  557. args: [youtubeId, likedSongsPlaylist]
  558. },
  559. this
  560. )
  561. .then(res => {
  562. if (res.status === "error")
  563. return next("Unable to remove song from the 'Liked Songs' playlist.");
  564. return next(null, song);
  565. })
  566. .catch(err => next(err));
  567. },
  568. (song, next) => {
  569. MediaModule.runJob("RECALCULATE_RATINGS", { youtubeId })
  570. .then(ratings => next(null, song, ratings))
  571. .catch(err => next(err));
  572. }
  573. ],
  574. async (err, song, ratings) => {
  575. if (err) {
  576. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  577. this.log(
  578. "ERROR",
  579. "MEDIA_RATINGS_UNLIKE",
  580. `User "${session.userId}" failed to unlike song ${youtubeId}. "${err}"`
  581. );
  582. return cb({ status: "error", message: err });
  583. }
  584. const { likes, dislikes } = ratings;
  585. if (song._id) SongsModule.runJob("UPDATE_SONG", { songId: song._id });
  586. CacheModule.runJob("PUB", {
  587. channel: "ratings.unlike",
  588. value: JSON.stringify({
  589. youtubeId,
  590. userId: session.userId,
  591. likes,
  592. dislikes
  593. })
  594. });
  595. ActivitiesModule.runJob("ADD_ACTIVITY", {
  596. userId: session.userId,
  597. type: "song__unlike",
  598. payload: {
  599. message: `Removed <youtubeId>${song.title} by ${song.artists.join(
  600. ", "
  601. )}</youtubeId> from your Liked Songs`,
  602. youtubeId,
  603. thumbnail: song.thumbnail
  604. }
  605. });
  606. return cb({
  607. status: "success",
  608. message: "You have successfully unliked this song."
  609. });
  610. }
  611. );
  612. }),
  613. /**
  614. * Get ratings
  615. *
  616. * @param session
  617. * @param youtubeId - the youtube id
  618. * @param cb
  619. */
  620. getRatings: isLoginRequired(async function getRatings(session, youtubeId, cb) {
  621. async.waterfall(
  622. [
  623. next => {
  624. MediaModule.runJob("GET_RATINGS", { youtubeId, createMissing: true }, this)
  625. .then(res => next(null, res.ratings))
  626. .catch(next);
  627. },
  628. (ratings, next) => {
  629. next(null, {
  630. likes: ratings.likes,
  631. dislikes: ratings.dislikes
  632. });
  633. }
  634. ],
  635. async (err, ratings) => {
  636. if (err) {
  637. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  638. this.log(
  639. "ERROR",
  640. "MEDIA_GET_RATINGS",
  641. `User "${session.userId}" failed to get ratings for ${youtubeId}. "${err}"`
  642. );
  643. return cb({ status: "error", message: err });
  644. }
  645. const { likes, dislikes } = ratings;
  646. return cb({
  647. status: "success",
  648. data: {
  649. likes,
  650. dislikes
  651. }
  652. });
  653. }
  654. );
  655. }),
  656. /**
  657. * Gets user's own ratings
  658. *
  659. * @param session
  660. * @param youtubeId - the youtube id
  661. * @param cb
  662. */
  663. getOwnRatings: isLoginRequired(async function getOwnRatings(session, youtubeId, cb) {
  664. const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
  665. async.waterfall(
  666. [
  667. next => {
  668. MediaModule.runJob(
  669. "GET_MEDIA",
  670. {
  671. youtubeId
  672. },
  673. this
  674. )
  675. .then(() => next())
  676. .catch(next);
  677. },
  678. next =>
  679. playlistModel.findOne(
  680. { createdBy: session.userId, displayName: "Liked Songs" },
  681. (err, playlist) => {
  682. if (err) return next(err);
  683. if (!playlist) return next("'Liked Songs' playlist does not exist.");
  684. let isLiked = false;
  685. Object.values(playlist.songs).forEach(song => {
  686. // song is found in 'liked songs' playlist
  687. if (song.youtubeId === youtubeId) isLiked = true;
  688. });
  689. return next(null, isLiked);
  690. }
  691. ),
  692. (isLiked, next) =>
  693. playlistModel.findOne(
  694. { createdBy: session.userId, displayName: "Disliked Songs" },
  695. (err, playlist) => {
  696. if (err) return next(err);
  697. if (!playlist) return next("'Disliked Songs' playlist does not exist.");
  698. const ratings = { isLiked, isDisliked: false };
  699. Object.values(playlist.songs).forEach(song => {
  700. // song is found in 'disliked songs' playlist
  701. if (song.youtubeId === youtubeId) ratings.isDisliked = true;
  702. });
  703. return next(null, ratings);
  704. }
  705. )
  706. ],
  707. async (err, ratings) => {
  708. if (err) {
  709. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  710. this.log(
  711. "ERROR",
  712. "MEDIA_GET_OWN_RATINGS",
  713. `User "${session.userId}" failed to get ratings for ${youtubeId}. "${err}"`
  714. );
  715. return cb({ status: "error", message: err });
  716. }
  717. const { isLiked, isDisliked } = ratings;
  718. return cb({
  719. status: "success",
  720. data: {
  721. youtubeId,
  722. liked: isLiked,
  723. disliked: isDisliked
  724. }
  725. });
  726. }
  727. );
  728. })
  729. };