musora-content-services 1.0.155 → 1.0.157

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.github/workflows/node.js.yml +0 -0
  2. package/CHANGELOG.md +4 -0
  3. package/README.md +0 -0
  4. package/babel.config.js +0 -0
  5. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  6. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  7. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  8. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  9. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  10. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  11. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  12. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  13. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  14. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -0
  15. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  16. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  17. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  18. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  19. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -0
  20. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  21. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  22. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  23. package/docs/scripts/collapse.js +0 -0
  24. package/docs/scripts/commonNav.js +0 -0
  25. package/docs/scripts/linenumber.js +0 -0
  26. package/docs/scripts/nav.js +0 -0
  27. package/docs/scripts/polyfill.js +0 -0
  28. package/docs/scripts/prettify/Apache-License-2.0.txt +0 -0
  29. package/docs/scripts/prettify/lang-css.js +0 -0
  30. package/docs/scripts/prettify/prettify.js +0 -0
  31. package/docs/scripts/search.js +0 -0
  32. package/docs/styles/jsdoc.css +0 -0
  33. package/docs/styles/prettify.css +0 -0
  34. package/jest.config.js +0 -0
  35. package/jsdoc.json +0 -0
  36. package/link_mcs.sh +0 -0
  37. package/package.json +1 -1
  38. package/src/contentMetaData.js +0 -0
  39. package/src/filterBuilder.js +22 -26
  40. package/src/index.d.ts +10 -1
  41. package/src/index.js +10 -1
  42. package/src/services/config.js +0 -0
  43. package/src/services/contentLikes.js +0 -0
  44. package/src/services/contentProgress.js +31 -25
  45. package/src/services/dataContext.js +3 -3
  46. package/src/services/lastUpdated.js +18 -0
  47. package/src/services/railcontent.js +35 -33
  48. package/src/services/sanity.js +558 -555
  49. package/src/services/userPermissions.js +26 -0
  50. package/test/contentLikes.test.js +2 -3
  51. package/test/contentProgress.test.js +7 -7
  52. package/test/initializeTests.js +23 -0
  53. package/test/lastUpdated.test.js +22 -0
  54. package/test/localStorageMock.js +0 -0
  55. package/test/log.js +0 -0
  56. package/test/sanityQueryService.test.js +29 -58
  57. package/test/userPermissions.test.js +19 -0
  58. package/tools/generate-index.js +0 -0
File without changes
package/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [1.0.157](https://github.com/railroadmedia/musora-content-services/compare/v1.0.156...v1.0.157) (2024-11-08)
6
+
7
+ ### [1.0.156](https://github.com/railroadmedia/musora-content-services/compare/v1.0.155...v1.0.156) (2024-11-06)
8
+
5
9
  ### [1.0.155](https://github.com/railroadmedia/musora-content-services/compare/v1.0.154...v1.0.155) (2024-11-05)
6
10
 
7
11
  ### [1.0.154](https://github.com/railroadmedia/musora-content-services/compare/v1.0.153...v1.0.154) (2024-11-05)
package/README.md CHANGED
File without changes
package/babel.config.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/jest.config.js CHANGED
File without changes
package/jsdoc.json CHANGED
File without changes
package/link_mcs.sh CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "1.0.155",
3
+ "version": "1.0.157",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
File without changes
@@ -1,3 +1,4 @@
1
+ import {fetchUserPermissions} from "./services/userPermissions";
1
2
 
2
3
 
3
4
  export class FilterBuilder {
@@ -5,17 +6,15 @@ export class FilterBuilder {
5
6
  STATUS_SCHEDULED = 'scheduled';
6
7
 
7
8
  constructor(
8
- filter = '',
9
- {
10
- user = undefined,
11
- availableContentStatuses = [],
12
- bypassPermissions = false,
13
- pullFutureContent = false,
14
- getFutureContentOnly = false,
15
- getFutureScheduledContentsOnly = false,
16
-
17
- }={}) {
18
- this.user = user;
9
+ filter = '',
10
+ {
11
+ availableContentStatuses = [],
12
+ bypassPermissions = false,
13
+ pullFutureContent = false,
14
+ getFutureContentOnly = false,
15
+ getFutureScheduledContentsOnly = false,
16
+
17
+ } = {}) {
19
18
  this.availableContentStatuses = availableContentStatuses;
20
19
  this.bypassPermissions = bypassPermissions;
21
20
  this.pullFutureContent = pullFutureContent;
@@ -27,13 +26,15 @@ export class FilterBuilder {
27
26
  }
28
27
 
29
28
 
30
- static withOnlyFilterAvailableStatuses(filter, availableContentStatuses) {
31
- return new FilterBuilder(filter,{
32
- availableContentStatuses,
33
- });
29
+ static withOnlyFilterAvailableStatuses(filter, availableContentStatuses, bypassPermissions) {
30
+ return new FilterBuilder(filter, {
31
+ availableContentStatuses,
32
+ bypassPermissions,
33
+ });
34
34
  }
35
35
 
36
- buildFilter() {
36
+ async buildFilter() {
37
+ this.userData = await fetchUserPermissions();
37
38
  if (this.debug) console.log('baseFilter', this.filter);
38
39
  const filter = this
39
40
  ._applyContentStatuses()
@@ -64,20 +65,15 @@ export class FilterBuilder {
64
65
  }
65
66
 
66
67
  _applyPermissions() {
67
- if (this.bypassPermissions) return this;
68
- // TODO these need to be pulled from the user and reference either ID, or railcontent_id
68
+ if (this.bypassPermissions || this.userData.isAdmin) return this;
69
69
  const requiredPermissions = this._getUserPermissions();
70
70
  if (requiredPermissions.length === 0) return this;
71
- // handle pullSongsContent, I think the flagging on this needs to be backwards compared to BE
72
- // if using id, switch railcontent_id to _id in the below query
73
71
  this._andWhere(`references(*[_type == 'permission' && railcontent_id in ${arrayToRawRepresentation(requiredPermissions)}]._id)`);
74
72
  return this;
75
-
76
73
  }
77
74
 
78
75
  _getUserPermissions() {
79
- // TODO need user store up and running to complete this, until then just null check
80
- return this?.user?.permissions ?? [];
76
+ return this.userData.permissions;
81
77
  }
82
78
 
83
79
  _applyPublishingDateRestrictions() {
@@ -88,7 +84,7 @@ export class FilterBuilder {
88
84
  this._andWhere(`published_on <= '${now}'`);
89
85
  } else {
90
86
  const date = new Date();
91
- const theFuture = new Date(date.setMonth(date.getMonth() + 18));
87
+ const theFuture = new Date(date.setMonth(date.getMonth() + 18));
92
88
  this._andWhere(`published_on <= '${theFuture}'`);
93
89
  }
94
90
  return this;
@@ -106,8 +102,8 @@ export class FilterBuilder {
106
102
 
107
103
  _trimAmpersands() {
108
104
  this.filter = this.filter.trim();
109
- while( this.filter.charAt(0) === '&' || this.filter.charAt(0) === ' ' ) this.filter = this.filter.substring(1);
110
- while( this.filter.charAt(this.filter.length) === '&' || this.filter.charAt(this.filter.length) === ' ' ) this.filter = this.filter.slice(-1);
105
+ while (this.filter.charAt(0) === '&' || this.filter.charAt(0) === ' ') this.filter = this.filter.substring(1);
106
+ while (this.filter.charAt(this.filter.length) === '&' || this.filter.charAt(this.filter.length) === ' ') this.filter = this.filter.slice(-1);
111
107
  return this;
112
108
  }
113
109
 
package/src/index.d.ts CHANGED
@@ -43,9 +43,10 @@ import {
43
43
  fetchPlaylistItems,
44
44
  fetchSongsInProgress,
45
45
  fetchUserAward,
46
+ fetchUserBadges,
46
47
  fetchUserChallengeProgress,
47
48
  fetchUserLikes,
48
- fetchUserPermissions,
49
+ fetchUserPermissionsData,
49
50
  fetchUserPlaylists,
50
51
  likePlaylist,
51
52
  postChallengesCommunityNotification,
@@ -114,6 +115,11 @@ import {
114
115
  getSortOrder
115
116
  } from './services/sanity.js';
116
117
 
118
+ import {
119
+ fetchUserPermissions,
120
+ reset
121
+ } from './services/userPermissions.js';
122
+
117
123
  declare module 'musora-content-services' {
118
124
  export {
119
125
  contentStatusCompleted,
@@ -183,9 +189,11 @@ declare module 'musora-content-services' {
183
189
  fetchTopLevelParentId,
184
190
  fetchUpcomingEvents,
185
191
  fetchUserAward,
192
+ fetchUserBadges,
186
193
  fetchUserChallengeProgress,
187
194
  fetchUserLikes,
188
195
  fetchUserPermissions,
196
+ fetchUserPermissionsData,
189
197
  fetchUserPlaylists,
190
198
  fetchWorkouts,
191
199
  getProgressPercentage,
@@ -212,6 +220,7 @@ declare module 'musora-content-services' {
212
220
  postContentUnliked,
213
221
  postRecordWatchSession,
214
222
  recordWatchSession,
223
+ reset,
215
224
  unlikeContent,
216
225
  updatePlaylist,
217
226
  updatePlaylistItem,
package/src/index.js CHANGED
@@ -43,9 +43,10 @@ import {
43
43
  fetchPlaylistItems,
44
44
  fetchSongsInProgress,
45
45
  fetchUserAward,
46
+ fetchUserBadges,
46
47
  fetchUserChallengeProgress,
47
48
  fetchUserLikes,
48
- fetchUserPermissions,
49
+ fetchUserPermissionsData,
49
50
  fetchUserPlaylists,
50
51
  likePlaylist,
51
52
  postChallengesCommunityNotification,
@@ -114,6 +115,11 @@ import {
114
115
  getSortOrder
115
116
  } from './services/sanity.js';
116
117
 
118
+ import {
119
+ fetchUserPermissions,
120
+ reset
121
+ } from './services/userPermissions.js';
122
+
117
123
  export {
118
124
  contentStatusCompleted,
119
125
  contentStatusReset,
@@ -182,9 +188,11 @@ export {
182
188
  fetchTopLevelParentId,
183
189
  fetchUpcomingEvents,
184
190
  fetchUserAward,
191
+ fetchUserBadges,
185
192
  fetchUserChallengeProgress,
186
193
  fetchUserLikes,
187
194
  fetchUserPermissions,
195
+ fetchUserPermissionsData,
188
196
  fetchUserPlaylists,
189
197
  fetchWorkouts,
190
198
  getProgressPercentage,
@@ -211,6 +219,7 @@ export {
211
219
  postContentUnliked,
212
220
  postRecordWatchSession,
213
221
  recordWatchSession,
222
+ reset,
214
223
  unlikeContent,
215
224
  updatePlaylist,
216
225
  updatePlaylistItem,
File without changes
File without changes
@@ -75,9 +75,9 @@ function completeStatusInLocalContext(contentId, localContext, hierarchy) {
75
75
  export async function contentStatusReset(contentId) {
76
76
  await dataContext.update(
77
77
  function (localContext) {
78
- const index = localContext.data.indexOf(contentId);
78
+ const index = Object.keys(localContext.data).indexOf(contentId);
79
79
  if (index > -1) { // only splice array when item is found
80
- localContext.data.splice(index, 1); // 2nd parameter means remove one item only
80
+ delete localContext.data[contentId];
81
81
  }
82
82
  },
83
83
  async function () {
@@ -86,31 +86,24 @@ export async function contentStatusReset(contentId) {
86
86
  }
87
87
 
88
88
 
89
- export async function recordWatchSession({
90
- mediaId,
91
- mediaType,
92
- mediaCategory,
93
- watchPositionSeconds,
94
- totalDurationSeconds,
95
- sessionToken,
96
- brand,
97
- contentId = null
98
- }) {
89
+ export async function recordWatchSession(contentId, mediaType, mediaCategory, mediaLengthSeconds, currentSeconds, secondsPlayed, sessionId = null) {
90
+ let mediaTypeId = getMediaTypeId(mediaType, mediaCategory);
91
+ let updateLocalProgress = mediaTypeId === 1 || mediaTypeId === 2; //only update for video playback
99
92
  await dataContext.update(
100
93
  async function (localContext) {
101
- if (contentId) {
94
+ if (contentId && updateLocalProgress ) {
102
95
  let data = localContext.data[contentId] ?? [];
103
96
  let progress = data?.[DATA_KEY_PROGRESS] ?? 0;
104
97
  let status = data?.[DATA_KEY_STATUS] ?? 0;
105
98
 
106
99
  if (status !== STATE_COMPLETED && progress !== 100) {
107
100
  status = STATE_STARTED;
108
- progress = Math.min(99, Math.round(watchPositionSeconds ?? 0 / Math.max(1, totalDurationSeconds ?? 0) * 100));
101
+ progress = Math.min(99, Math.round(currentSeconds ?? 0 / Math.max(1, mediaLengthSeconds ?? 0) * 100));
109
102
  }
110
103
 
111
104
  data[DATA_KEY_PROGRESS] = progress;
112
105
  data[DATA_KEY_STATUS] = status;
113
- data[DATA_KEY_RESUME_TIME] = watchPositionSeconds;
106
+ data[DATA_KEY_RESUME_TIME] = currentSeconds;
114
107
  localContext.data[contentId] = data;
115
108
 
116
109
  let hierarchy = await fetchHierarchy(contentId);
@@ -118,18 +111,31 @@ export async function recordWatchSession({
118
111
  }
119
112
  },
120
113
  async function () {
121
- return postRecordWatchSession({
122
- mediaId,
123
- mediaType,
124
- mediaCategory,
125
- watchPositionSeconds,
126
- totalDurationSeconds,
127
- sessionToken,
128
- brand,
129
- contentId
130
- });
114
+ return postRecordWatchSession(contentId, mediaTypeId, mediaLengthSeconds, currentSeconds, secondsPlayed, sessionId);
131
115
  }
132
116
  );
117
+ return sessionId;
118
+ }
119
+
120
+ function getMediaTypeId(mediaType, mediaCategory) {
121
+ switch (`${mediaType}_${mediaCategory}`) {
122
+ case "video_youtube":
123
+ return 1;
124
+ case "video_vimeo":
125
+ return 2;
126
+ case "assignment_soundslice":
127
+ return 3;
128
+ case "practice_play-alongs":
129
+ return 4;
130
+ default:
131
+ throw Error(`Unsupported media type: ${mediaType}_${mediaCategory}`);
132
+ }
133
+ }
134
+
135
+ function uuidv4() {
136
+ return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
137
+ (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16)
138
+ );
133
139
  }
134
140
 
135
141
  function bubbleProgress(hierarchy, contentId, localContext) {
@@ -34,7 +34,7 @@ export class DataContext {
34
34
  this.context = data;
35
35
  cache.setItem(this.localStorageKey, JSON.stringify(data));
36
36
  }
37
- cache.setItem(this.localStorageLastUpdatedKey, new Date().getTime());
37
+ cache.setItem(this.localStorageLastUpdatedKey, new Date().getTime()?.toString());
38
38
  }
39
39
  return this.context.data;
40
40
  }
@@ -83,10 +83,10 @@ export class DataContext {
83
83
  this.context.version++;
84
84
  let data = JSON.stringify(this.context);
85
85
  cache.setItem(this.localStorageKey, data);
86
- cache.setItem(this.localStorageLastUpdatedKey, new Date().getTime());
86
+ cache.setItem(this.localStorageLastUpdatedKey, new Date().getTime().toString());
87
87
  }
88
88
  let response = await serverUpdateFunction();
89
- if (response.version !== this.version()) {
89
+ if (response?.version !== this.version()) {
90
90
  this.clearCache();
91
91
  }
92
92
  }
@@ -0,0 +1,18 @@
1
+ import {globalConfig} from "./config";
2
+
3
+ /**
4
+ * Exported functions that are excluded from index generation.
5
+ *
6
+ * @type {string[]}
7
+ */
8
+ const excludeFromGeneratedIndex = ['wasLastUpdateOlderThanXSeconds', 'setLastUpdatedTime'];
9
+ export function wasLastUpdateOlderThanXSeconds(seconds, key) {
10
+ let lastUpdated = globalConfig.localStorage.getItem(key);
11
+ if (!lastUpdated) return false;
12
+ const verifyServerTime = seconds * 1000;
13
+ return (new Date().getTime() - lastUpdated) > verifyServerTime;
14
+ }
15
+
16
+ export function setLastUpdatedTime(key) {
17
+ globalConfig.localStorage.setItem(key, new Date().getTime()?.toString());
18
+ }
@@ -16,7 +16,8 @@ const excludeFromGeneratedIndex = [
16
16
  'postRecordWatchSession',
17
17
  'postContentStarted',
18
18
  'postContentCompleted',
19
- 'postContentReset'
19
+ 'postContentReset',
20
+ 'fetchUserPermissionsData'
20
21
  ];
21
22
 
22
23
 
@@ -236,10 +237,10 @@ export async function fetchContentPageUserData(contentId) {
236
237
  }
237
238
  }
238
239
 
239
- export async function fetchUserPermissions() {
240
- let url = `/content/user_data_permissions`;
240
+ export async function fetchUserPermissionsData() {
241
+ let url = `/content/user/permissions`;
241
242
  // in the case of an unauthorized user, we return empty permissions
242
- return fetchHandler(url, 'get') ?? [];
243
+ return await fetchHandler(url, 'get') ?? [];
243
244
  }
244
245
 
245
246
  async function fetchDataHandler(url, dataVersion, method = "get") {
@@ -265,11 +266,11 @@ export async function fetchHandler(url, method = "get", dataVersion = null, body
265
266
  }
266
267
  try {
267
268
  const response = await fetchAbsolute(url, options);
268
- const result = await response.json();
269
- if (result) {
270
- return result;
269
+ if (response.ok) {
270
+ return await response.json();
271
271
  } else {
272
- console.log('result not json');
272
+ console.log('fetch error:', response.status);
273
+ console.log(response);
273
274
  }
274
275
  } catch (error) {
275
276
  console.error('Fetch error:', error);
@@ -297,26 +298,15 @@ export async function fetchContentProgress(currentVersion) {
297
298
  return fetchDataHandler(url, currentVersion);
298
299
  }
299
300
 
300
- export async function postRecordWatchSession({
301
- mediaId,
302
- mediaType,
303
- mediaCategory,
304
- watchPosition,
305
- totalDuration,
306
- sessionToken,
307
- brand,
308
- contentId = null
309
- }) {
310
- let url = `/railtracker/media-playback-session`;
301
+ export async function postRecordWatchSession(contentId, mediaTypeId, mediaLengthSeconds, currentSeconds, secondsPlayed, sessionId) {
302
+ let url = `/v2/railtracker/media-playback-session`;
311
303
  return postDataHandler(url, {
312
- mediaId,
313
- mediaType,
314
- mediaCategory,
315
- watchPosition,
316
- totalDuration,
317
- sessionToken,
318
- brand,
319
- contentId
304
+ "content_id": contentId,
305
+ "media_type_id": mediaTypeId,
306
+ "media_length_seconds": mediaLengthSeconds,
307
+ "current_second": currentSeconds,
308
+ "seconds_played": secondsPlayed,
309
+ "session_id": sessionId
320
310
  });
321
311
  }
322
312
 
@@ -365,7 +355,7 @@ export async function fetchUserAward(contentId) {
365
355
  }
366
356
 
367
357
  /**
368
- * Get challange duration, user progress, and status for the list of challenges
358
+ * Get challenge duration, user progress, and status for the list of challenges
369
359
  * Intended to be used on the index page for challenges
370
360
  *
371
361
  * @param {array} contentIds - arary of railcontent ids of the challenges
@@ -377,6 +367,18 @@ export async function fetchChallengeIndexMetadata(contentIds) {
377
367
  return await fetchHandler(url, 'get');
378
368
  }
379
369
 
370
+ /**
371
+ * Fetch all completed badges for the user ordered by completion date descending
372
+ *
373
+ * @param {string|null} brand -
374
+ * @returns {Promise<any|null>}
375
+ */
376
+ export async function fetchUserBadges(brand = null) {
377
+ let brandParam = brand ? `?brand=${brand}` : '';
378
+ let url = `/challenges/user_badges/get${brandParam}`;
379
+ return await fetchHandler(url, 'get');
380
+ }
381
+
380
382
  /**
381
383
  * Enroll a user in a challenge and set the start date of the challenge to the provided day.
382
384
  * Clears any existing progress data for this challenge
@@ -481,8 +483,8 @@ export async function fetchUserPlaylists(brand, {page, limit, sort, searchTerm}
481
483
  let url;
482
484
  const limitString = limit ? `&limit=${limit}` : '';
483
485
  const pageString = page ? `&page=${page}` : '';
484
- const sortString = sort ? `&sort=${sort}`:'';
485
- const searchFilter = searchTerm ? `&term=${searchTerm}`: '';
486
+ const sortString = sort ? `&sort=${sort}` : '';
487
+ const searchFilter = searchTerm ? `&term=${searchTerm}` : '';
486
488
  url = `/playlists/all?brand=${brand}${limitString}${pageString}${sortString}${searchFilter}`;
487
489
  return await fetchHandler(url);
488
490
  }
@@ -517,7 +519,7 @@ export async function fetchUserPlaylists(brand, {page, limit, sort, searchTerm}
517
519
  */
518
520
  export async function duplicatePlaylist(playlistId, playlistData) {
519
521
  let url = `/playlists/duplicate/${playlistId}`;
520
- return await fetchHandler(url, "post",null, playlistData);
522
+ return await fetchHandler(url, "post", null, playlistData);
521
523
  }
522
524
 
523
525
  /**
@@ -643,7 +645,7 @@ export async function createPlaylist(playlistData) {
643
645
  */
644
646
  export async function likePlaylist(playlistId) {
645
647
  const url = `/playlists/like`;
646
- const payload = { playlist_id: playlistId };
648
+ const payload = {playlist_id: playlistId};
647
649
  return await fetchHandler(url, "PUT", null, payload);
648
650
  }
649
651
 
@@ -670,7 +672,7 @@ export async function likePlaylist(playlistId) {
670
672
  */
671
673
  export async function deletePlaylistLike(playlistId) {
672
674
  const url = `/playlists/like`;
673
- const payload = { playlist_id: playlistId };
675
+ const payload = {playlist_id: playlistId};
674
676
  return await fetchHandler(url, "DELETE", null, payload);
675
677
  }
676
678