musora-content-services 2.26.1 → 2.27.1

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
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
+ ### [2.27.1](https://github.com/railroadmedia/musora-content-services/compare/v2.27.0...v2.27.1) (2025-07-17)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **Userpractice:** add category_id and instrument_id to userPractice ([2c92c6f](https://github.com/railroadmedia/musora-content-services/commit/2c92c6f6adcb7fac657c3b12e1c3f5e927cff214))
11
+
12
+ ## [2.27.0](https://github.com/railroadmedia/musora-content-services/compare/v2.24.0...v2.27.0) (2025-07-17)
13
+
14
+
15
+ ### Features
16
+
17
+ * **BEH-38:** add context data to tab results ([#344](https://github.com/railroadmedia/musora-content-services/issues/344)) ([02fe932](https://github.com/railroadmedia/musora-content-services/commit/02fe9321dfa56e74d7c73cb7a80e54f5818d1427))
18
+ * **MU2-805:** Set a pauseLiveEventPollingUntil field when user mark all notificatio… ([#351](https://github.com/railroadmedia/musora-content-services/issues/351)) ([bfe1010](https://github.com/railroadmedia/musora-content-services/commit/bfe10107c522ea725436e4a6338a869e852c08d5))
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+ * Add live event fallback to unread notifications count ([61afc83](https://github.com/railroadmedia/musora-content-services/commit/61afc835a7f009ec5e12947b2bf2131b7ed7049a))
24
+ * T3PS-21: Live Stream content cards in the Progress Row and Recent Lessons ([3736c48](https://github.com/railroadmedia/musora-content-services/commit/3736c4898d9db27869de079660474e6dd0233da4))
25
+ * T3PS-259: Incorrect stats for past months ([b38ee32](https://github.com/railroadmedia/musora-content-services/commit/b38ee320488196115ff4ef97d4ea4ea661d1eb43))
26
+ * user data endpoint ([#356](https://github.com/railroadmedia/musora-content-services/issues/356)) ([2a00bb8](https://github.com/railroadmedia/musora-content-services/commit/2a00bb859faa72ec22930919f4a636275b219772))
27
+
5
28
  ### [2.26.1](https://github.com/railroadmedia/musora-content-services/compare/v2.26.0...v2.26.1) (2025-07-16)
6
29
 
7
30
  ## [2.26.0](https://github.com/railroadmedia/musora-content-services/compare/v2.25.1...v2.26.0) (2025-07-16)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.26.1",
3
+ "version": "2.27.1",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/index.d.ts CHANGED
@@ -261,13 +261,14 @@ import {
261
261
 
262
262
  import {
263
263
  deleteNotification,
264
+ fetchLiveEventPollingState,
264
265
  fetchNotificationSettings,
265
266
  fetchNotifications,
266
267
  fetchUnreadCount,
267
268
  markAllNotificationsAsRead,
268
269
  markNotificationAsRead,
269
270
  markNotificationAsUnread,
270
- pauseLiveEventPollingUntil,
271
+ pauseLiveEventPolling,
271
272
  startLiveEventPolling,
272
273
  updateNotificationSetting
273
274
  } from './services/user/notifications.js';
@@ -382,6 +383,7 @@ declare module 'musora-content-services' {
382
383
  fetchLessonsFeaturingThisContent,
383
384
  fetchLikeCount,
384
385
  fetchLiveEvent,
386
+ fetchLiveEventPollingState,
385
387
  fetchMetadata,
386
388
  fetchMethod,
387
389
  fetchMethodChildren,
@@ -488,7 +490,7 @@ declare module 'musora-content-services' {
488
490
  markNotificationAsUnread,
489
491
  openComment,
490
492
  otherStats,
491
- pauseLiveEventPollingUntil,
493
+ pauseLiveEventPolling,
492
494
  pinGuidedCourse,
493
495
  pinProgressRow,
494
496
  pinnedGuidedCourses,
package/src/index.js CHANGED
@@ -261,13 +261,14 @@ import {
261
261
 
262
262
  import {
263
263
  deleteNotification,
264
+ fetchLiveEventPollingState,
264
265
  fetchNotificationSettings,
265
266
  fetchNotifications,
266
267
  fetchUnreadCount,
267
268
  markAllNotificationsAsRead,
268
269
  markNotificationAsRead,
269
270
  markNotificationAsUnread,
270
- pauseLiveEventPollingUntil,
271
+ pauseLiveEventPolling,
271
272
  startLiveEventPolling,
272
273
  updateNotificationSetting
273
274
  } from './services/user/notifications.js';
@@ -381,6 +382,7 @@ export {
381
382
  fetchLessonsFeaturingThisContent,
382
383
  fetchLikeCount,
383
384
  fetchLiveEvent,
385
+ fetchLiveEventPollingState,
384
386
  fetchMetadata,
385
387
  fetchMethod,
386
388
  fetchMethodChildren,
@@ -487,7 +489,7 @@ export {
487
489
  markNotificationAsUnread,
488
490
  openComment,
489
491
  otherStats,
490
- pauseLiveEventPollingUntil,
492
+ pauseLiveEventPolling,
491
493
  pinGuidedCourse,
492
494
  pinProgressRow,
493
495
  pinnedGuidedCourses,
@@ -371,6 +371,8 @@ function resetStatusInLocalContext(localContext, contentId, hierarchy) {
371
371
  * @param {int} currentSeconds
372
372
  * @param {int} secondsPlayed
373
373
  * @param {string} sessionId - This function records a sessionId to pass into future updates to progress on the same video
374
+ * @param {int} instrumentId - enum value of instrument id
375
+ * @param {int} categoryId - enum value of category id
374
376
  */
375
377
  export async function recordWatchSession(
376
378
  contentId,
@@ -379,7 +381,9 @@ export async function recordWatchSession(
379
381
  mediaLengthSeconds,
380
382
  currentSeconds,
381
383
  secondsPlayed,
382
- sessionId = null
384
+ sessionId = null,
385
+ instrumentId = null,
386
+ categoryId = null,
383
387
  ) {
384
388
  let mediaTypeId = getMediaTypeId(mediaType, mediaCategory)
385
389
  let updateLocalProgress = mediaTypeId === 1 || mediaTypeId === 2 //only update for video playback
@@ -391,7 +395,7 @@ export async function recordWatchSession(
391
395
  //TODO: Good enough for Alpha, Refine in reliability improvements
392
396
  sessionData[sessionId] = sessionData[sessionId] || {}
393
397
  const secondsSinceLastUpdate = Math.ceil(secondsPlayed - (sessionData[sessionId][contentId] ?? 0))
394
- await recordUserPractice({ content_id: contentId, duration_seconds: secondsSinceLastUpdate })
398
+ await recordUserPractice({ content_id: contentId, duration_seconds: secondsSinceLastUpdate, category_id: categoryId, instrument_id: instrumentId })
395
399
  } catch (error) {
396
400
  console.error('Failed to record user practice:', error)
397
401
  }
@@ -520,13 +520,30 @@ export async function fetchByRailContentIds(ids, contentType = undefined, brand
520
520
  }
521
521
  const idsString = ids.join(',')
522
522
  const brandFilter = brand ? ` && brand == "${brand}"` : ''
523
+ const now = getSanityDate(new Date())
523
524
  const query = `*[
524
525
  railcontent_id in [${idsString}]${brandFilter}
525
526
  ]{
526
527
  ${getFieldsForContentType(contentType)}
528
+ live_event_start_time,
529
+ live_event_end_time,
527
530
  }`
528
531
 
529
- const results = await fetchSanity(query, true)
532
+ const customPostProcess = (results) => {
533
+ const now = getSanityDate(new Date(), false);
534
+ const liveProcess = (result) => {
535
+ if (result.live_event_start_time && result.live_event_end_time) {
536
+ result.isLive =
537
+ result.live_event_start_time <= now &&
538
+ result.live_event_end_time >= now;
539
+ } else {
540
+ result.isLive = false;
541
+ }
542
+ return result;
543
+ };
544
+ return results.map(liveProcess);
545
+ }
546
+ const results = await fetchSanity(query, true, { customPostProcess: customPostProcess })
530
547
 
531
548
  const sortFuction = function compare(a, b) {
532
549
  const indexA = ids.indexOf(a['id'])
@@ -2385,6 +2402,7 @@ export async function fetchTabData(
2385
2402
  }
2386
2403
 
2387
2404
  const fieldsString = getFieldsForContentType('tab-data');
2405
+ const now = getSanityDate(new Date())
2388
2406
 
2389
2407
  // Determine the group by clause
2390
2408
  let query = ''
@@ -2396,6 +2414,7 @@ export async function fetchTabData(
2396
2414
  entityFieldsString =
2397
2415
  ` ${fieldsString}
2398
2416
  'children': child[${childrenFilter}]->{'id': railcontent_id},
2417
+ 'isLive': live_event_start_time <= "${now}" && live_event_end_time >= "${now}",
2399
2418
  'lesson_count': coalesce(count(child[${childrenFilter}]->), 0),
2400
2419
  'length_in_seconds': coalesce(
2401
2420
  math::sum(
@@ -60,21 +60,15 @@ export async function markNotificationAsRead(notificationId) {
60
60
  }
61
61
 
62
62
  /**
63
- * Marks all notifications as read for a specific brand.
63
+ * Marks all notifications as read for the current user.
64
64
  *
65
- * @returns {Promise<any>} - A promise that resolves when all notifications are marked as read.
65
+ * This also pauses live event polling if there is an active event, to prevent immediate re-polling.
66
66
  *
67
- * @example
68
- * markAllNotificationsAsRead('drumeo')
69
- * .then(response => console.log(response))
70
- * .catch(error => console.error(error));
67
+ * @param {string} [brand='drumeo'] - The brand context for live event handling before marking notifications.
68
+ * @returns {Promise<Object>} - A promise resolving to the API response from the notifications read endpoint.
71
69
  */
72
70
  export async function markAllNotificationsAsRead(brand = 'drumeo') {
73
- const liveEvent = await fetchLiveEvent(brand)
74
- if(liveEvent){
75
- await pauseLiveEventPollingUntil(liveEvent.live_event_end_time)
76
- }
77
-
71
+ await pauseLiveEventPolling(brand)
78
72
  const url = `${baseUrl}/v1/read`
79
73
  return fetchHandler(url, 'put')
80
74
  }
@@ -128,20 +122,33 @@ export async function deleteNotification(notificationId) {
128
122
  /**
129
123
  * Fetches the count of unread notifications for the current user in a given brand context.
130
124
  *
125
+ * This function first checks for standard unread notifications. If none are found,
126
+ * it checks if live event polling is active. If so, it will query for any ongoing live events.
127
+ * If a live event is active, it counts as an unread item.
128
+ *
131
129
  * @param {Object} [options={}] - Options for fetching unread count.
132
- * @param {string} options.brand - The brand to filter unread notifications by (required).
133
- * @returns {Promise<Object>} - A promise that resolves to an object with the unread count.
130
+ * @param {string} options.brand - The brand to filter unread notifications by. Defaults to 'drumeo'.
131
+ * @returns {Promise<Object>} - A promise that resolves to an object with a `data` property indicating the unread count (0 or 1).
134
132
  *
135
- * @throws {Error} If the brand is not provided.
133
+ * @throws {Error} If the brand is not provided or if network requests fail.
136
134
  *
137
135
  * @example
138
136
  * fetchUnreadCount({ brand: 'drumeo' })
139
- * .then(data => console.log(data.unread_count))
137
+ * .then(data => console.log(data.data)) // 0 or 1
140
138
  * .catch(error => console.error(error));
141
139
  */
142
- export async function fetchUnreadCount({ brand = null} = {}) {
140
+ export async function fetchUnreadCount({ brand = 'drumeo'} = {}) {
143
141
  const url = `${baseUrl}/v1/unread-count`
144
- return fetchHandler(url, 'get')
142
+ const notifUnread = await fetchHandler(url, 'get')
143
+ if (notifUnread.data > 0) {
144
+ return notifUnread// Return early if unread notifications exist
145
+ }
146
+ const liveEventPollingState = await fetchLiveEventPollingState()
147
+ if(liveEventPollingState.data?.read_state === true){
148
+ const liveEvent = await fetchLiveEvent(brand)
149
+ return { data: liveEvent ? 1 : 0}
150
+ }
151
+ return notifUnread
145
152
  }
146
153
 
147
154
  /**
@@ -231,20 +238,26 @@ export async function updateNotificationSetting({ brand, settingName, email, pus
231
238
  }
232
239
 
233
240
  /**
234
- * Pauses live event polling until the specified time.
235
- * @param {string|null} [until=null] - ISO timestamp string or null to unpause
236
- * @returns {Promise<Object>} - Promise resolving to the API response
241
+ * Pauses live event polling for the current user based on the live event end time.
242
+ *
243
+ * If a live event is active, polling will be paused until its end time. If no live event is found,
244
+ * polling is not paused.
245
+ *
246
+ * @param {string} [brand='drumeo'] - The brand context to fetch live event data for.
247
+ * @returns {Promise<Object>} - A promise resolving to the API response from the pause polling endpoint.
237
248
  */
238
- export async function pauseLiveEventPollingUntil(until = null) {
239
- const url = `/api/user-management-system/v1/users/pause-polling${until ? `?until=${until}` : ''}`
240
- return fetchHandler(url, 'PUT', null)
249
+ export async function pauseLiveEventPolling(brand = 'drumeo') {
250
+ const liveEvent = await fetchLiveEvent(brand)
251
+ const until = liveEvent?.live_event_end_time || null
252
+ const url = `/api/user-management-system/v1/users/pause-polling${until ? `?until=${until}` : ''}`
253
+ return fetchHandler(url, 'PUT', null)
241
254
  }
242
255
 
243
256
  /**
244
257
  * Start live event polling.
245
258
  * @returns {Promise<Object>} - Promise resolving to the API response
246
259
  */
247
- export async function startLiveEventPolling() {
260
+ export async function startLiveEventPolling(brand = 'drumeo') {
248
261
  const url = `/api/user-management-system/v1/users/start-polling`
249
262
  return fetchHandler(url, 'PUT', null)
250
263
  }
@@ -254,10 +267,10 @@ export async function startLiveEventPolling() {
254
267
  + @returns {Promise<Object>} - Promise resolving to the polling state
255
268
  */
256
269
 
257
- export async function fetchLiveEventPollingState() {
270
+ export async function fetchLiveEventPollingState() {
258
271
  const url = `/api/user-management-system/v1/users/polling`
259
272
  return fetchHandler(url, 'GET', null)
260
- }
273
+ }
261
274
 
262
275
 
263
276
 
@@ -174,11 +174,11 @@ export async function getUserMonthlyStats(params = {}) {
174
174
  }
175
175
 
176
176
  let endOfMonth = new Date(year, month + 1, 0)
177
- while (endOfMonth.getDay() !== 0) {
178
- endOfMonth.setDate(endOfMonth.getDate() + 1)
177
+ let endOfGrid = new Date(year, month + 1, 0)
178
+ while (endOfGrid.getDay() !== 0) {
179
+ endOfGrid.setDate(endOfGrid.getDate() + 1)
179
180
  }
180
-
181
- let daysInMonth = Math.ceil((endOfMonth - startOfGrid) / (1000 * 60 * 60 * 24)) + 1
181
+ let daysInMonth = Math.ceil((endOfGrid - startOfGrid) / (1000 * 60 * 60 * 24)) + 1
182
182
 
183
183
  let dailyStats = []
184
184
  let practiceDuration = 0
@@ -272,7 +272,9 @@ export async function getUserMonthlyStats(params = {}) {
272
272
  * auto: false,
273
273
  * category_id: 5,
274
274
  * title: "Guitar Warm-up",
275
- * thumbnail_url: "https://example.com/thumbnail.jpg"
275
+ * thumbnail_url: "https://example.com/thumbnail.jpg",
276
+ * instrument_id: 1,
277
+ * instrument_id: 2,
276
278
  * })
277
279
  * .then(response => console.log(response))
278
280
  * .catch(error => console.error(error));
@@ -1073,6 +1075,7 @@ async function processContentItem(item) {
1073
1075
  let data = item.raw;
1074
1076
  const contentType = getFormattedType(data.type, data.brand);
1075
1077
  const status = item.state;
1078
+ const isLive = data.isLive ?? false
1076
1079
  let ctaText = 'Continue';
1077
1080
  if (contentType === 'transcription' || contentType === 'play-along' || contentType === 'jam-track') ctaText = 'Replay Song';
1078
1081
  if (contentType === 'lesson') ctaText = status === 'completed' ? 'Revisit Lesson' : 'Continue';
@@ -1159,13 +1162,14 @@ async function processContentItem(item) {
1159
1162
  header: contentType,
1160
1163
  pinned: item.pinned ?? false,
1161
1164
  body: {
1162
- progressPercent: item.percent,
1165
+ progressPercent: isLive ? undefined: item.percent,
1163
1166
  thumbnail: data.thumbnail,
1164
1167
  title: data.title,
1168
+ isLive: isLive,
1165
1169
  badge: data.badge ?? null,
1166
1170
  isLocked: data.is_locked ?? false,
1167
1171
  subtitle: !data.child_count || data.lesson_count === 1
1168
- ? (contentType === 'lesson') ? `${item.percent}% Complete`: `${data.difficulty_string} • ${data.artist_name}`
1172
+ ? (contentType === 'lesson' && isLive === false) ? `${item.percent}% Complete`: `${data.difficulty_string} • ${data.artist_name}`
1169
1173
  : `${data.completed_children} of ${data.lesson_count ?? data.child_count} Lessons Complete`
1170
1174
  },
1171
1175
  cta: {