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 +23 -0
- package/package.json +1 -1
- package/src/index.d.ts +4 -2
- package/src/index.js +4 -2
- package/src/services/contentProgress.js +6 -2
- package/src/services/sanity.js +20 -1
- package/src/services/user/notifications.js +39 -26
- package/src/services/userActivity.js +11 -7
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
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/services/sanity.js
CHANGED
|
@@ -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
|
|
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
|
|
63
|
+
* Marks all notifications as read for the current user.
|
|
64
64
|
*
|
|
65
|
-
*
|
|
65
|
+
* This also pauses live event polling if there is an active event, to prevent immediate re-polling.
|
|
66
66
|
*
|
|
67
|
-
* @
|
|
68
|
-
*
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 =
|
|
140
|
+
export async function fetchUnreadCount({ brand = 'drumeo'} = {}) {
|
|
143
141
|
const url = `${baseUrl}/v1/unread-count`
|
|
144
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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: {
|