musora-content-services 2.30.0 → 2.30.4
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 +19 -0
- package/package.json +1 -1
- package/src/services/contentProgress.js +72 -43
- package/src/services/sanity.js +2 -0
- package/src/services/userActivity.js +22 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
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.30.4](https://github.com/railroadmedia/musora-content-services/compare/v2.30.3...v2.30.4) (2025-08-05)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **MU2-912:** pagination for Incomplete and Completed tabs ([#396](https://github.com/railroadmedia/musora-content-services/issues/396)) ([b66d574](https://github.com/railroadmedia/musora-content-services/commit/b66d57483df4bdd158a985bade3cf63fa3aa65bb))
|
|
11
|
+
* **MU2-916:** Add navigateTo and parentId to practice tracker and recent activity ([#398](https://github.com/railroadmedia/musora-content-services/issues/398)) ([6e43768](https://github.com/railroadmedia/musora-content-services/commit/6e437682f09655cc41966277c967c304b275d0ea))
|
|
12
|
+
|
|
13
|
+
### [2.30.3](https://github.com/railroadmedia/musora-content-services/compare/v2.30.2...v2.30.3) (2025-08-04)
|
|
14
|
+
|
|
15
|
+
### [2.30.2](https://github.com/railroadmedia/musora-content-services/compare/v2.30.1...v2.30.2) (2025-08-04)
|
|
16
|
+
|
|
17
|
+
### [2.30.1](https://github.com/railroadmedia/musora-content-services/compare/v2.30.0...v2.30.1) (2025-08-04)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* null check on buildNavigateTo ([#397](https://github.com/railroadmedia/musora-content-services/issues/397)) ([2387877](https://github.com/railroadmedia/musora-content-services/commit/238787786fa322872f733d769547a49796fc0bf1))
|
|
23
|
+
|
|
5
24
|
## [2.30.0](https://github.com/railroadmedia/musora-content-services/compare/v2.28.6...v2.30.0) (2025-08-01)
|
|
6
25
|
|
|
7
26
|
|
package/package.json
CHANGED
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
postRecordWatchSession,
|
|
6
6
|
} from './railcontent.js'
|
|
7
7
|
import { DataContext, ContentProgressVersionKey } from './dataContext.js'
|
|
8
|
-
import {fetchHierarchy} from './sanity.js'
|
|
9
|
-
import {recordUserPractice, findIncompleteLesson} from
|
|
10
|
-
import {getNextLessonLessonParentTypes} from
|
|
8
|
+
import { fetchHierarchy } from './sanity.js'
|
|
9
|
+
import { recordUserPractice, findIncompleteLesson } from './userActivity'
|
|
10
|
+
import { getNextLessonLessonParentTypes } from '../contentTypeConfig.js'
|
|
11
11
|
|
|
12
12
|
const STATE_STARTED = 'started'
|
|
13
13
|
const STATE_COMPLETED = 'completed'
|
|
@@ -45,22 +45,19 @@ export async function getResumeTimeSecondsByIds(contentIds) {
|
|
|
45
45
|
return getByIds(contentIds, DATA_KEY_RESUME_TIME, 0)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export async function getNextLesson(data)
|
|
49
|
-
{
|
|
48
|
+
export async function getNextLesson(data) {
|
|
50
49
|
let nextLessonData = {}
|
|
51
50
|
|
|
52
51
|
for (const content of data) {
|
|
53
|
-
const children = content.children?.map(child => child.id) ?? []
|
|
52
|
+
const children = content.children?.map((child) => child.id) ?? []
|
|
54
53
|
//only calculate nextLesson if needed, based on content type
|
|
55
54
|
if (!getNextLessonLessonParentTypes.includes(content.type)) {
|
|
56
55
|
nextLessonData[content.id] = null
|
|
57
|
-
|
|
58
56
|
} else {
|
|
59
57
|
//return first child if parent-content is complete or no progress
|
|
60
58
|
const contentState = await getProgressState(content.id)
|
|
61
59
|
if (contentState !== STATE_STARTED) {
|
|
62
60
|
nextLessonData[content.id] = children[0]
|
|
63
|
-
|
|
64
61
|
} else {
|
|
65
62
|
const childrenStates = await getProgressStateByIds(children)
|
|
66
63
|
|
|
@@ -73,16 +70,23 @@ export async function getNextLesson(data)
|
|
|
73
70
|
if (lastInteractedStatus === STATE_STARTED) {
|
|
74
71
|
nextLessonData[content.id] = lastInteracted
|
|
75
72
|
} else {
|
|
76
|
-
nextLessonData[content.id] = findIncompleteLesson(
|
|
73
|
+
nextLessonData[content.id] = findIncompleteLesson(
|
|
74
|
+
childrenStates,
|
|
75
|
+
lastInteracted,
|
|
76
|
+
content.type
|
|
77
|
+
)
|
|
77
78
|
}
|
|
78
|
-
|
|
79
79
|
} else if (content.type === 'guided-course' || content.type === 'song-tutorial') {
|
|
80
|
-
nextLessonData[content.id] = findIncompleteLesson(
|
|
80
|
+
nextLessonData[content.id] = findIncompleteLesson(
|
|
81
|
+
childrenStates,
|
|
82
|
+
lastInteracted,
|
|
83
|
+
content.type
|
|
84
|
+
)
|
|
81
85
|
} else if (content.type === 'pack') {
|
|
82
86
|
const packBundles = content.children ?? []
|
|
83
87
|
const packBundleProgressData = await getNextLesson(packBundles)
|
|
84
|
-
const parentId = await getLastInteractedOf(packBundles.map(bundle => bundle.id))
|
|
85
|
-
nextLessonData[content.id] = packBundleProgressData[parentId]
|
|
88
|
+
const parentId = await getLastInteractedOf(packBundles.map((bundle) => bundle.id))
|
|
89
|
+
nextLessonData[content.id] = packBundleProgressData[parentId]
|
|
86
90
|
}
|
|
87
91
|
}
|
|
88
92
|
}
|
|
@@ -90,31 +94,32 @@ export async function getNextLesson(data)
|
|
|
90
94
|
return nextLessonData
|
|
91
95
|
}
|
|
92
96
|
|
|
93
|
-
export async function getNavigateTo(data)
|
|
94
|
-
{
|
|
97
|
+
export async function getNavigateTo(data) {
|
|
95
98
|
let navigateToData = {}
|
|
96
99
|
const twoDepthContentTypes = ['pack'] //TODO add method when we know what it's called
|
|
97
100
|
//TODO add parent hierarchy upwards as well
|
|
98
101
|
// data structure is the same but instead of child{} we use parent{}
|
|
99
102
|
for (const content of data) {
|
|
100
|
-
|
|
101
103
|
//only calculate nextLesson if needed, based on content type
|
|
102
104
|
if (!getNextLessonLessonParentTypes.includes(content.type) || !content.children) {
|
|
103
105
|
navigateToData[content.id] = null
|
|
104
106
|
} else {
|
|
105
107
|
const children = new Map()
|
|
106
108
|
const childrenIds = []
|
|
107
|
-
content.children.forEach(child => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
109
|
+
content.children.forEach((child) => {
|
|
110
|
+
childrenIds.push(child.id)
|
|
111
|
+
children.set(child.id, child)
|
|
112
|
+
})
|
|
112
113
|
// return first child (or grand child) if parent-content is complete or no progress
|
|
113
114
|
const contentState = await getProgressState(content.id)
|
|
114
115
|
if (contentState !== STATE_STARTED) {
|
|
115
116
|
const firstChild = content.children[0]
|
|
116
|
-
let lastInteractedChildNavToData =
|
|
117
|
-
|
|
117
|
+
let lastInteractedChildNavToData =
|
|
118
|
+
(await getNavigateTo([firstChild])[firstChild.id]) ?? null
|
|
119
|
+
navigateToData[content.id] = buildNavigateTo(
|
|
120
|
+
content.children[0],
|
|
121
|
+
lastInteractedChildNavToData
|
|
122
|
+
)
|
|
118
123
|
} else {
|
|
119
124
|
const childrenStates = await getProgressStateByIds(childrenIds)
|
|
120
125
|
const lastInteracted = await getLastInteractedOf(childrenIds)
|
|
@@ -132,13 +137,18 @@ export async function getNavigateTo(data)
|
|
|
132
137
|
navigateToData[content.id] = buildNavigateTo(children.get(incompleteChild))
|
|
133
138
|
} else if (twoDepthContentTypes.includes(content.type)) {
|
|
134
139
|
const firstChildren = content.children ?? []
|
|
135
|
-
const lastInteractedChildId = await getLastInteractedOf(
|
|
140
|
+
const lastInteractedChildId = await getLastInteractedOf(
|
|
141
|
+
firstChildren.map((child) => child.id)
|
|
142
|
+
)
|
|
136
143
|
if (childrenStates[lastInteractedChildId] === STATE_COMPLETED) {
|
|
137
144
|
// TODO: packs have an extra situation where we need to jump to the next course if all lessons in the last engaged course are completed
|
|
138
145
|
}
|
|
139
146
|
let lastInteractedChildNavToData = await getNavigateTo(firstChildren)
|
|
140
147
|
lastInteractedChildNavToData = lastInteractedChildNavToData[lastInteractedChildId]
|
|
141
|
-
navigateToData[content.id] = buildNavigateTo(
|
|
148
|
+
navigateToData[content.id] = buildNavigateTo(
|
|
149
|
+
children.get(lastInteractedChildId),
|
|
150
|
+
lastInteractedChildNavToData
|
|
151
|
+
)
|
|
142
152
|
}
|
|
143
153
|
}
|
|
144
154
|
}
|
|
@@ -146,13 +156,16 @@ export async function getNavigateTo(data)
|
|
|
146
156
|
return navigateToData
|
|
147
157
|
}
|
|
148
158
|
|
|
149
|
-
function buildNavigateTo(content, child = null)
|
|
150
|
-
{
|
|
159
|
+
function buildNavigateTo(content, child = null) {
|
|
160
|
+
if (!content) {
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
|
|
151
164
|
return {
|
|
152
|
-
brand: content.brand,
|
|
165
|
+
brand: content.brand ?? '',
|
|
153
166
|
thumbnail: content.thumbnail ?? '',
|
|
154
|
-
id: content.id,
|
|
155
|
-
type: content.type,
|
|
167
|
+
id: content.id ?? null,
|
|
168
|
+
type: content.type ?? '',
|
|
156
169
|
child: child,
|
|
157
170
|
}
|
|
158
171
|
}
|
|
@@ -182,10 +195,14 @@ export async function getLastInteractedOf(contentIds) {
|
|
|
182
195
|
export async function getProgressDateByIds(contentIds) {
|
|
183
196
|
let data = await dataContext.getData()
|
|
184
197
|
let progress = {}
|
|
185
|
-
contentIds?.forEach(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
198
|
+
contentIds?.forEach(
|
|
199
|
+
(id) =>
|
|
200
|
+
(progress[id] = {
|
|
201
|
+
last_update: data[id]?.[DATA_KEY_LAST_UPDATED_TIME] ?? 0,
|
|
202
|
+
progress: data[id]?.[DATA_KEY_PROGRESS] ?? 0,
|
|
203
|
+
status: data[id]?.[DATA_KEY_STATUS] ?? '',
|
|
204
|
+
})
|
|
205
|
+
)
|
|
189
206
|
return progress
|
|
190
207
|
}
|
|
191
208
|
|
|
@@ -245,11 +262,16 @@ export async function getAllCompleted(limit = null) {
|
|
|
245
262
|
return ids
|
|
246
263
|
}
|
|
247
264
|
|
|
248
|
-
export async function getAllStartedOrCompleted({
|
|
265
|
+
export async function getAllStartedOrCompleted({
|
|
266
|
+
limit = null,
|
|
267
|
+
onlyIds = true,
|
|
268
|
+
brand = null,
|
|
269
|
+
excludedIds = [],
|
|
270
|
+
} = {}) {
|
|
249
271
|
const data = await dataContext.getData()
|
|
250
272
|
const oneMonthAgoInSeconds = Math.floor(Date.now() / 1000) - 60 * 24 * 60 * 60 // 60 days in seconds
|
|
251
273
|
|
|
252
|
-
const excludedSet = new Set(excludedIds.map(id => parseInt(id))) // ensure IDs are numbers
|
|
274
|
+
const excludedSet = new Set(excludedIds.map((id) => parseInt(id))) // ensure IDs are numbers
|
|
253
275
|
|
|
254
276
|
let filtered = Object.entries(data)
|
|
255
277
|
.filter(([key, item]) => {
|
|
@@ -307,13 +329,14 @@ export async function getAllStartedOrCompleted({ limit = null, onlyIds = true, b
|
|
|
307
329
|
* const progressMap = await getStartedOrCompletedProgressOnly({ brand: 'drumeo' });
|
|
308
330
|
* console.log(progressMap[123]); // => 52
|
|
309
331
|
*/
|
|
310
|
-
export async function getStartedOrCompletedProgressOnly({ brand = null} = {}) {
|
|
332
|
+
export async function getStartedOrCompletedProgressOnly({ brand = null } = {}) {
|
|
311
333
|
const data = await dataContext.getData()
|
|
312
334
|
const result = {}
|
|
313
335
|
|
|
314
336
|
Object.entries(data).forEach(([key, item]) => {
|
|
315
337
|
const id = parseInt(key)
|
|
316
|
-
const isRelevantStatus =
|
|
338
|
+
const isRelevantStatus =
|
|
339
|
+
item[DATA_KEY_STATUS] === STATE_STARTED || item[DATA_KEY_STATUS] === STATE_COMPLETED
|
|
317
340
|
const isCorrectBrand = !brand || item.b === brand
|
|
318
341
|
|
|
319
342
|
if (isRelevantStatus && isCorrectBrand) {
|
|
@@ -427,7 +450,7 @@ export async function recordWatchSession(
|
|
|
427
450
|
secondsPlayed,
|
|
428
451
|
sessionId = null,
|
|
429
452
|
instrumentId = null,
|
|
430
|
-
categoryId = null
|
|
453
|
+
categoryId = null
|
|
431
454
|
) {
|
|
432
455
|
let mediaTypeId = getMediaTypeId(mediaType, mediaCategory)
|
|
433
456
|
let updateLocalProgress = mediaTypeId === 1 || mediaTypeId === 2 //only update for video playback
|
|
@@ -438,10 +461,16 @@ export async function recordWatchSession(
|
|
|
438
461
|
try {
|
|
439
462
|
//TODO: Good enough for Alpha, Refine in reliability improvements
|
|
440
463
|
sessionData[sessionId] = sessionData[sessionId] || {}
|
|
441
|
-
const secondsSinceLastUpdate = Math.ceil(
|
|
442
|
-
|
|
464
|
+
const secondsSinceLastUpdate = Math.ceil(
|
|
465
|
+
secondsPlayed - (sessionData[sessionId][contentId] ?? 0)
|
|
466
|
+
)
|
|
467
|
+
await recordUserPractice({
|
|
468
|
+
content_id: contentId,
|
|
469
|
+
duration_seconds: secondsSinceLastUpdate,
|
|
470
|
+
instrument_id: instrumentId,
|
|
471
|
+
})
|
|
443
472
|
} catch (error) {
|
|
444
|
-
|
|
473
|
+
console.error('Failed to record user practice:', error)
|
|
445
474
|
}
|
|
446
475
|
sessionData[sessionId][contentId] = secondsPlayed
|
|
447
476
|
|
|
@@ -506,7 +535,7 @@ function bubbleProgress(hierarchy, contentId, localContext) {
|
|
|
506
535
|
return localContext.data[childId]?.[DATA_KEY_PROGRESS] ?? 0
|
|
507
536
|
})
|
|
508
537
|
let progress = Math.round(childProgress.reduce((a, b) => a + b, 0) / childProgress.length)
|
|
509
|
-
const brand =localContext.data[contentId]?.[DATA_KEY_BRAND] ?? null
|
|
538
|
+
const brand = localContext.data[contentId]?.[DATA_KEY_BRAND] ?? null
|
|
510
539
|
data[DATA_KEY_PROGRESS] = progress
|
|
511
540
|
data[DATA_KEY_STATUS] = progress === 100 ? STATE_COMPLETED : STATE_STARTED
|
|
512
541
|
data[DATA_KEY_LAST_UPDATED_TIME] = Math.round(new Date().getTime() / 1000)
|
package/src/services/sanity.js
CHANGED
|
@@ -2173,10 +2173,12 @@ export async function fetchTabData(
|
|
|
2173
2173
|
case 'incomplete':
|
|
2174
2174
|
progressIds = await getAllStarted();
|
|
2175
2175
|
sortOrder = null;
|
|
2176
|
+
withoutPagination = true;
|
|
2176
2177
|
break;
|
|
2177
2178
|
case 'completed':
|
|
2178
2179
|
progressIds = await getAllCompleted();
|
|
2179
2180
|
sortOrder = null;
|
|
2181
|
+
withoutPagination = true;
|
|
2180
2182
|
break;
|
|
2181
2183
|
}
|
|
2182
2184
|
|
|
@@ -407,7 +407,7 @@ export async function restoreUserPractice(id) {
|
|
|
407
407
|
const restoredPractice = response.data.find((p) => p.id === id)
|
|
408
408
|
if (restoredPractice) {
|
|
409
409
|
await userActivityContext.updateLocal(async function (localContext) {
|
|
410
|
-
if (localContext.data[DATA_KEY_PRACTICES][restoredPractice.day]) {
|
|
410
|
+
if (!localContext.data[DATA_KEY_PRACTICES][restoredPractice.day]) {
|
|
411
411
|
localContext.data[DATA_KEY_PRACTICES][restoredPractice.day] = []
|
|
412
412
|
}
|
|
413
413
|
response.data.forEach((restoredPractice) => {
|
|
@@ -581,7 +581,21 @@ export async function getPracticeNotes(day) {
|
|
|
581
581
|
* .catch(error => console.error("Failed to get recent activity:", error));
|
|
582
582
|
*/
|
|
583
583
|
export async function getRecentActivity({ page = 1, limit = 5, tabName = null } = {}) {
|
|
584
|
-
|
|
584
|
+
const recentActivityData = await fetchRecentUserActivities({ page, limit, tabName })
|
|
585
|
+
const contentIds = recentActivityData.data.map((p) => p.contentId).filter((id) => id !== null)
|
|
586
|
+
const contents = await addContextToContent(fetchByRailContentIds, contentIds, {
|
|
587
|
+
addNavigateTo: true,
|
|
588
|
+
addNextLesson: true
|
|
589
|
+
})
|
|
590
|
+
recentActivityData.data = recentActivityData.data.map((practice) => {
|
|
591
|
+
const content = contents.find((c) => c.id === practice.contentId) || {}
|
|
592
|
+
return {
|
|
593
|
+
...practice,
|
|
594
|
+
parent_id: content.parent_id || null,
|
|
595
|
+
navigateTo: content.navigateTo,
|
|
596
|
+
}
|
|
597
|
+
})
|
|
598
|
+
return recentActivityData
|
|
585
599
|
}
|
|
586
600
|
|
|
587
601
|
/**
|
|
@@ -841,8 +855,10 @@ export async function calculateLongestStreaks(userId = globalConfig.sessionConfi
|
|
|
841
855
|
|
|
842
856
|
async function formatPracticeMeta(practices) {
|
|
843
857
|
const contentIds = practices.map((p) => p.content_id).filter((id) => id !== null)
|
|
844
|
-
const contents = await fetchByRailContentIds
|
|
845
|
-
|
|
858
|
+
const contents = await addContextToContent(fetchByRailContentIds, contentIds, {
|
|
859
|
+
addNavigateTo: true,
|
|
860
|
+
addNextLesson: true
|
|
861
|
+
})
|
|
846
862
|
return practices.map((practice) => {
|
|
847
863
|
const content = contents.find((c) => c.id === practice.content_id) || {}
|
|
848
864
|
|
|
@@ -863,6 +879,8 @@ async function formatPracticeMeta(practices) {
|
|
|
863
879
|
created_at: dayjs(practice.created_at),
|
|
864
880
|
sanity_type: content.type || null,
|
|
865
881
|
content_slug: content.slug || null,
|
|
882
|
+
parent_id: content.parent_id || null,
|
|
883
|
+
navigateTo: content.navigateTo || null,
|
|
866
884
|
}
|
|
867
885
|
})
|
|
868
886
|
}
|