musora-content-services 2.28.6 → 2.30.0
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 +38 -0
- package/link_mcs.sh +0 -0
- package/package.json +1 -1
- package/src/contentTypeConfig.js +69 -50
- package/src/services/config.js +0 -6
- package/src/services/content-org/playlists-types.js +10 -0
- package/src/services/content-org/playlists.js +14 -14
- package/src/services/content.js +8 -9
- package/src/services/contentAggregator.js +45 -7
- package/src/services/contentProgress.js +67 -0
- package/src/services/gamification/awards.js +52 -33
- package/src/services/gamification/types.js +5 -23
- package/src/services/railcontent.js +25 -36
- package/src/services/recommendations.js +20 -18
- package/src/services/sanity.js +27 -57
- package/src/services/types.js +0 -8
- package/src/services/userActivity.js +340 -419
- package/test/initializeTests.js +0 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,44 @@
|
|
|
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.0](https://github.com/railroadmedia/musora-content-services/compare/v2.28.6...v2.30.0) (2025-08-01)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **navigateTo:** Add navigateTo context and update homepageprogress rows to pull the same data as other locations ([#389](https://github.com/railroadmedia/musora-content-services/issues/389)) ([b731957](https://github.com/railroadmedia/musora-content-services/commit/b73195790ba0aefd98e1584ef0f57839979613b3))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* Change recommendation config to use recommender.musora.com ([#395](https://github.com/railroadmedia/musora-content-services/issues/395)) ([170d48a](https://github.com/railroadmedia/musora-content-services/commit/170d48a4b10bb11371889e14a74b3dd663d5c0e7))
|
|
16
|
+
* **MU2-907:** scheduled content appearing in new releases ([#391](https://github.com/railroadmedia/musora-content-services/issues/391)) ([c4f3c48](https://github.com/railroadmedia/musora-content-services/commit/c4f3c482c62513ddc4b2465bb7a6e155a92a52da))
|
|
17
|
+
* **T3PS-120:** allow falsey values for description and category ([#392](https://github.com/railroadmedia/musora-content-services/issues/392)) ([5645488](https://github.com/railroadmedia/musora-content-services/commit/56454880df8c92fa3c9c0a57f8b8ae7115d49391))
|
|
18
|
+
* **T3PS-139:** fix lesson count on content cards ([#383](https://github.com/railroadmedia/musora-content-services/issues/383)) ([818c4d0](https://github.com/railroadmedia/musora-content-services/commit/818c4d0e5f8903d9f7b2a7667460a4beb144d7ab))
|
|
19
|
+
* **T3PS-594:** wrong active date mobile ([#387](https://github.com/railroadmedia/musora-content-services/issues/387)) ([f9e2dfd](https://github.com/railroadmedia/musora-content-services/commit/f9e2dfd008660d55865e526317ef79e5d95e3f06))
|
|
20
|
+
* **T3PS-602:** fixes thumbnail data missing from getNewAndUpcoming MCS method ([#386](https://github.com/railroadmedia/musora-content-services/issues/386)) ([f8da5fc](https://github.com/railroadmedia/musora-content-services/commit/f8da5fc674380ffdebcf636ddbb7b0a5f14ba7ae))
|
|
21
|
+
* **T3PS-630:** show song-tutorial children in recent songs instead of parent ([#390](https://github.com/railroadmedia/musora-content-services/issues/390)) ([312bdf8](https://github.com/railroadmedia/musora-content-services/commit/312bdf891a6ffb6eb3113ebfea1692d586e0d2ea))
|
|
22
|
+
* **T3PS-644:** practice day incorrectly assigned ([#393](https://github.com/railroadmedia/musora-content-services/issues/393)) ([8b8a424](https://github.com/railroadmedia/musora-content-services/commit/8b8a4245cdaa389c7916eb398da47bf30f6fc056))
|
|
23
|
+
|
|
24
|
+
## [2.29.0](https://github.com/railroadmedia/musora-content-services/compare/v2.28.6...v2.29.0) (2025-08-01)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Features
|
|
28
|
+
|
|
29
|
+
* **navigateTo:** Add navigateTo context and update homepageprogress rows to pull the same data as other locations ([#389](https://github.com/railroadmedia/musora-content-services/issues/389)) ([b731957](https://github.com/railroadmedia/musora-content-services/commit/b73195790ba0aefd98e1584ef0f57839979613b3))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Bug Fixes
|
|
33
|
+
|
|
34
|
+
* Change recommendation config to use recommender.musora.com ([#395](https://github.com/railroadmedia/musora-content-services/issues/395)) ([170d48a](https://github.com/railroadmedia/musora-content-services/commit/170d48a4b10bb11371889e14a74b3dd663d5c0e7))
|
|
35
|
+
* **MU2-907:** scheduled content appearing in new releases ([#391](https://github.com/railroadmedia/musora-content-services/issues/391)) ([c4f3c48](https://github.com/railroadmedia/musora-content-services/commit/c4f3c482c62513ddc4b2465bb7a6e155a92a52da))
|
|
36
|
+
* **T3PS-120:** allow falsey values for description and category ([#392](https://github.com/railroadmedia/musora-content-services/issues/392)) ([5645488](https://github.com/railroadmedia/musora-content-services/commit/56454880df8c92fa3c9c0a57f8b8ae7115d49391))
|
|
37
|
+
* **T3PS-139:** fix lesson count on content cards ([#383](https://github.com/railroadmedia/musora-content-services/issues/383)) ([818c4d0](https://github.com/railroadmedia/musora-content-services/commit/818c4d0e5f8903d9f7b2a7667460a4beb144d7ab))
|
|
38
|
+
* **T3PS-594:** wrong active date mobile ([#387](https://github.com/railroadmedia/musora-content-services/issues/387)) ([f9e2dfd](https://github.com/railroadmedia/musora-content-services/commit/f9e2dfd008660d55865e526317ef79e5d95e3f06))
|
|
39
|
+
* **T3PS-602:** fixes thumbnail data missing from getNewAndUpcoming MCS method ([#386](https://github.com/railroadmedia/musora-content-services/issues/386)) ([f8da5fc](https://github.com/railroadmedia/musora-content-services/commit/f8da5fc674380ffdebcf636ddbb7b0a5f14ba7ae))
|
|
40
|
+
* **T3PS-630:** show song-tutorial children in recent songs instead of parent ([#390](https://github.com/railroadmedia/musora-content-services/issues/390)) ([312bdf8](https://github.com/railroadmedia/musora-content-services/commit/312bdf891a6ffb6eb3113ebfea1692d586e0d2ea))
|
|
41
|
+
* **T3PS-644:** practice day incorrectly assigned ([#393](https://github.com/railroadmedia/musora-content-services/issues/393)) ([8b8a424](https://github.com/railroadmedia/musora-content-services/commit/8b8a4245cdaa389c7916eb398da47bf30f6fc056))
|
|
42
|
+
|
|
5
43
|
### [2.28.6](https://github.com/railroadmedia/musora-content-services/compare/v2.28.5...v2.28.6) (2025-07-28)
|
|
6
44
|
|
|
7
45
|
|
package/link_mcs.sh
CHANGED
|
File without changes
|
package/package.json
CHANGED
package/src/contentTypeConfig.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
//import {AWSUrl, CloudFrontURl} from "./services/config";
|
|
2
2
|
import {Tabs} from "./contentMetaData.js";
|
|
3
|
+
import {FilterBuilder} from "./filterBuilder.js";
|
|
3
4
|
|
|
4
5
|
export const AWSUrl = 'https://s3.us-east-1.amazonaws.com/musora-web-platform'
|
|
5
6
|
export const CloudFrontURl = 'https://d3fzm1tzeyr5n3.cloudfront.net'
|
|
@@ -34,12 +35,27 @@ export const DEFAULT_FIELDS = [
|
|
|
34
35
|
'"lesson_count": coalesce(count(child[]->.child[]->), child_count)',
|
|
35
36
|
'"parent_id": parent_content_data[0].id',
|
|
36
37
|
]
|
|
38
|
+
|
|
37
39
|
export const DEFAULT_CHILD_FIELDS = [
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
"'id': railcontent_id",
|
|
41
|
+
'railcontent_id',
|
|
42
|
+
artistOrInstructorName(),
|
|
43
|
+
"'artist': artist->{ 'name': name, 'thumbnail': thumbnail_url.asset->url}",
|
|
44
|
+
'title',
|
|
45
|
+
"'image': thumbnail.asset->url",
|
|
46
|
+
"'thumbnail': thumbnail.asset->url",
|
|
47
|
+
'difficulty',
|
|
48
|
+
'difficulty_string',
|
|
49
|
+
'published_on',
|
|
50
|
+
"'type': _type",
|
|
51
|
+
"'length_in_seconds' : coalesce(length_in_seconds, soundslice[0].soundslice_length_in_second)",
|
|
52
|
+
'brand',
|
|
53
|
+
"'genre': genre[]->name",
|
|
54
|
+
'status',
|
|
55
|
+
"'slug' : slug.current",
|
|
56
|
+
"'permission_id': permission[]->railcontent_id",
|
|
57
|
+
'child_count',
|
|
58
|
+
'"parent_id": parent_content_data[0].id',
|
|
43
59
|
]
|
|
44
60
|
|
|
45
61
|
export const instructorField = `instructor[]->{
|
|
@@ -202,7 +218,7 @@ export const progressTypesMapping = {
|
|
|
202
218
|
'show': showsLessonTypes,
|
|
203
219
|
'song tutorial': [...tutorialsLessonTypes, 'song-tutorial-children'],
|
|
204
220
|
'songs': transcriptionsLessonTypes,
|
|
205
|
-
'play
|
|
221
|
+
'play along': playAlongLessonTypes,
|
|
206
222
|
'guided course': ['guided-course'],
|
|
207
223
|
'pack': ['pack', 'semester-pack'],
|
|
208
224
|
'method': ['learning-path'],
|
|
@@ -224,7 +240,7 @@ export const filterTypes = {
|
|
|
224
240
|
|
|
225
241
|
export const recentTypes = {
|
|
226
242
|
lessons: [...individualLessonsTypes, 'course-part', 'pack-bundle-lesson', 'guided-course-part', 'quick-tips'],
|
|
227
|
-
songs: [...
|
|
243
|
+
songs: [...SONG_TYPES],
|
|
228
244
|
home: [...individualLessonsTypes, ...tutorialsLessonTypes, ...transcriptionsLessonTypes, ...playAlongLessonTypes,
|
|
229
245
|
'guided-course', 'learning-path', 'live', 'course', 'pack']
|
|
230
246
|
}
|
|
@@ -235,30 +251,14 @@ export let contentTypeConfig = {
|
|
|
235
251
|
'enrollment_start_time',
|
|
236
252
|
'enrollment_end_time',
|
|
237
253
|
],
|
|
254
|
+
includeChildFields: true,
|
|
238
255
|
},
|
|
239
256
|
'progress-tracker': {
|
|
240
257
|
fields: [
|
|
241
258
|
'"parent_content_data": parent_content_data[].id',
|
|
242
259
|
'"badge" : badge.asset->url',
|
|
243
|
-
'"lessons": child[]->{' +
|
|
244
|
-
'"id": railcontent_id,' +
|
|
245
|
-
'"slug":slug.current,' +
|
|
246
|
-
'"brand":brand,' +
|
|
247
|
-
'"type": _type,' +
|
|
248
|
-
'"thumbnail": thumbnail.asset->url,' +
|
|
249
|
-
'published_on,' +
|
|
250
|
-
'"lessons": child[]->{' +
|
|
251
|
-
'"id":railcontent_id,' +
|
|
252
|
-
'"slug":slug.current,' +
|
|
253
|
-
'"type": _type,' +
|
|
254
|
-
'"brand":brand},' +
|
|
255
|
-
'"thumbnail": thumbnail.asset->url,' +
|
|
256
|
-
'published_on,' +
|
|
257
|
-
'}',
|
|
258
260
|
],
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
261
|
+
includeChildFields: true,
|
|
262
262
|
},
|
|
263
263
|
song: {
|
|
264
264
|
fields: ['album', 'soundslice', 'instrumentless', `"resources": ${resourcesField}`],
|
|
@@ -282,6 +282,7 @@ export let contentTypeConfig = {
|
|
|
282
282
|
}`,
|
|
283
283
|
'"instructors": instructor[]->name',
|
|
284
284
|
],
|
|
285
|
+
includeChildFields: true,
|
|
285
286
|
relationships: {
|
|
286
287
|
artist: {
|
|
287
288
|
isOneToOne: true,
|
|
@@ -291,6 +292,9 @@ export let contentTypeConfig = {
|
|
|
291
292
|
'song-tutorial-children': {
|
|
292
293
|
fields: [`"resources": ${resourcesField}`],
|
|
293
294
|
},
|
|
295
|
+
'guided-course': {
|
|
296
|
+
includeChildFields: true,
|
|
297
|
+
},
|
|
294
298
|
course: {
|
|
295
299
|
fields: [
|
|
296
300
|
'"lesson_count": child_count',
|
|
@@ -424,26 +428,20 @@ export let contentTypeConfig = {
|
|
|
424
428
|
'"instructors": instructor[]->{ "id": railcontent_id, name, "thumbnail_url": thumbnail_url.asset->url }',
|
|
425
429
|
'"logo_image_url": logo_image_url.asset->url',
|
|
426
430
|
'total_xp',
|
|
427
|
-
`"children": child[]->{
|
|
428
|
-
"description": ${descriptionField},
|
|
429
|
-
"lesson_count": child_count,
|
|
430
|
-
"instructors": select(
|
|
431
|
-
instructor != null => instructor[]->name,
|
|
432
|
-
^.instructor[]->name
|
|
433
|
-
),
|
|
434
|
-
"children": child[]->{
|
|
435
|
-
"description": ${descriptionField},
|
|
436
|
-
"children": child[]->{"id": railcontent_id},
|
|
437
|
-
${getFieldsForContentType()}
|
|
438
|
-
},
|
|
439
|
-
${getFieldsForContentType()}
|
|
440
|
-
}`,
|
|
441
431
|
`"resources": ${resourcesField}`,
|
|
442
432
|
'"thumbnail": thumbnail.asset->url',
|
|
443
433
|
'"light_mode_logo": light_mode_logo_url.asset->url',
|
|
444
434
|
'"dark_mode_logo": dark_mode_logo_url.asset->url',
|
|
445
435
|
`"description": ${descriptionField}`,
|
|
446
436
|
],
|
|
437
|
+
childFields: [
|
|
438
|
+
`'description': ${descriptionField}`,
|
|
439
|
+
"'lesson_count': child_count",
|
|
440
|
+
`'instructors': select(
|
|
441
|
+
instructor != null => instructor[]->name,
|
|
442
|
+
^.instructor[]->name
|
|
443
|
+
)`,
|
|
444
|
+
],
|
|
447
445
|
},
|
|
448
446
|
rudiment: {
|
|
449
447
|
fields: ['sheet_music_thumbnail_url'],
|
|
@@ -456,10 +454,6 @@ export let contentTypeConfig = {
|
|
|
456
454
|
'pack-children': {
|
|
457
455
|
fields: [
|
|
458
456
|
'child_count',
|
|
459
|
-
`"children": child[]->{
|
|
460
|
-
"description": ${descriptionField},
|
|
461
|
-
${getFieldsForContentType()}
|
|
462
|
-
}`,
|
|
463
457
|
`"resources": ${resourcesField}`,
|
|
464
458
|
'"image": logo_image_url.asset->url',
|
|
465
459
|
'"thumbnail": thumbnail.asset->url',
|
|
@@ -468,6 +462,9 @@ export let contentTypeConfig = {
|
|
|
468
462
|
`"description": ${descriptionField}`,
|
|
469
463
|
'total_xp',
|
|
470
464
|
],
|
|
465
|
+
childFields: [
|
|
466
|
+
`"description": ${descriptionField}`,
|
|
467
|
+
]
|
|
471
468
|
},
|
|
472
469
|
'pack-bundle-lesson': {
|
|
473
470
|
fields: [`"resources": ${resourcesField}`],
|
|
@@ -681,6 +678,35 @@ export function artistOrInstructorNameAsArray(key = 'artists') {
|
|
|
681
678
|
return `'${key}': select(artist->name != null => [artist->name], instructor[]->name)`
|
|
682
679
|
}
|
|
683
680
|
|
|
681
|
+
export async function getFieldsForContentTypeWithFilteredChildren(contentType, asQueryString = true) {
|
|
682
|
+
const childFields = getChildFieldsForContentType(contentType, true)
|
|
683
|
+
const parentFields = getFieldsForContentType(contentType, false)
|
|
684
|
+
if (childFields) {
|
|
685
|
+
const childFilter = await new FilterBuilder('', {isChildrenFilter: true}).buildFilter()
|
|
686
|
+
parentFields.push(
|
|
687
|
+
`"children": child[${childFilter}]->{
|
|
688
|
+
${childFields}
|
|
689
|
+
"children": child[${childFilter}]->{
|
|
690
|
+
${childFields}
|
|
691
|
+
},
|
|
692
|
+
}`
|
|
693
|
+
)
|
|
694
|
+
}
|
|
695
|
+
return asQueryString ? parentFields.toString() + ',' : parentFields
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
export function getChildFieldsForContentType(contentType, asQueryString = true)
|
|
699
|
+
{
|
|
700
|
+
if (contentTypeConfig[contentType]?.childFields || contentTypeConfig[contentType]?.includeChildFields) {
|
|
701
|
+
const childFields = contentType
|
|
702
|
+
? DEFAULT_CHILD_FIELDS.concat(contentTypeConfig?.[contentType]?.childFields ?? [])
|
|
703
|
+
: DEFAULT_CHILD_FIELDS
|
|
704
|
+
return asQueryString ? childFields.toString() + ',' : childFields
|
|
705
|
+
} else {
|
|
706
|
+
return asQueryString ? '' : []
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
684
710
|
export function getFieldsForContentType(contentType, asQueryString = true) {
|
|
685
711
|
const fields = contentType
|
|
686
712
|
? DEFAULT_FIELDS.concat(contentTypeConfig?.[contentType]?.fields ?? [])
|
|
@@ -688,13 +714,6 @@ export function getFieldsForContentType(contentType, asQueryString = true) {
|
|
|
688
714
|
return asQueryString ? fields.toString() + ',' : fields
|
|
689
715
|
}
|
|
690
716
|
|
|
691
|
-
export function getChildFieldsForContentType(contentType, asQueryString = true) {
|
|
692
|
-
const fields = contentType
|
|
693
|
-
? DEFAULT_CHILD_FIELDS.concat(childContentTypeConfig?.[contentType] ?? [])
|
|
694
|
-
: DEFAULT_CHILD_FIELDS
|
|
695
|
-
return asQueryString ? fields.toString() + ',' : fields
|
|
696
|
-
}
|
|
697
|
-
|
|
698
717
|
/**
|
|
699
718
|
* Takes the included fields array and returns a string that can be used in a groq query.
|
|
700
719
|
* @param {Array<string>} filters - An array of strings that represent applied filters. This should be in the format of a key,value array. eg. ['difficulty,Intermediate',
|
package/src/services/config.js
CHANGED
|
@@ -8,7 +8,6 @@ import './types.js'
|
|
|
8
8
|
export let globalConfig = {
|
|
9
9
|
sanityConfig: {},
|
|
10
10
|
railcontentConfig: {},
|
|
11
|
-
recommendationsConfig: {},
|
|
12
11
|
sessionConfig: {},
|
|
13
12
|
localStorage: null,
|
|
14
13
|
isMA: false,
|
|
@@ -50,10 +49,6 @@ const excludeFromGeneratedIndex = []
|
|
|
50
49
|
* userId: 'current-user-id',
|
|
51
50
|
* authToken 'your-auth-token',
|
|
52
51
|
* },
|
|
53
|
-
* recommendationsConfig: {
|
|
54
|
-
* token: 'your-user-api-token',
|
|
55
|
-
* baseUrl: 'https://MusoraProductDepartment-PWGenerator.hf.space',
|
|
56
|
-
* },
|
|
57
52
|
* baseUrl: 'https://web-staging-one.musora.com',
|
|
58
53
|
* localStorage: localStorage,
|
|
59
54
|
* isMA: false,
|
|
@@ -67,5 +62,4 @@ export function initializeService(config) {
|
|
|
67
62
|
globalConfig.localStorage = config.localStorage
|
|
68
63
|
globalConfig.isMA = config.isMA || false
|
|
69
64
|
globalConfig.localTimezoneString = config.localTimezoneString || null
|
|
70
|
-
globalConfig.recommendationsConfig = config.recommendationsConfig
|
|
71
65
|
}
|
|
@@ -8,6 +8,16 @@
|
|
|
8
8
|
* @property {string} brand - Brand identifier for the playlist.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* @typedef UpdatePlaylistDTO
|
|
13
|
+
* @property {string} name - The name of the new playlist. (required, max 255 characters)
|
|
14
|
+
* @property {string} description - A description of the playlist. (optional, max 1000 characters)
|
|
15
|
+
* @property {string} category - The category of the playlist. (optional, max 255 characters)
|
|
16
|
+
* @property {boolean} is_private - Whether the playlist is private. (optional, defaults to false)
|
|
17
|
+
* @property {Array<number>} deleted_items - List of playlist items to be deleted. (optional)
|
|
18
|
+
* @property {Array<number>} item_order - List of all remaining playlist item ids (not content_ids) provided in the new order. (optional)
|
|
19
|
+
*/
|
|
20
|
+
|
|
11
21
|
/**
|
|
12
22
|
* @typedef DuplicatePlaylistDTO
|
|
13
23
|
* @property {string} name - The name of the new playlist. (required, max 255 characters)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { globalConfig } from '../config.js'
|
|
5
5
|
import { fetchHandler } from '../railcontent.js'
|
|
6
|
+
import { getNavigateToForPlaylists } from '../contentAggregator.js'
|
|
6
7
|
import './playlists-types.js'
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -42,7 +43,7 @@ export async function fetchUserPlaylists(
|
|
|
42
43
|
const content = content_id ? `&content_id=${content_id}` : ''
|
|
43
44
|
const brandString = brand ? `&brand=${brand}` : ''
|
|
44
45
|
const url = `${BASE_PATH}/v1/user/playlists${pageString}${brandString}${limitString}${sortString}${content}`
|
|
45
|
-
return await fetchHandler(url)
|
|
46
|
+
return await getNavigateToForPlaylists(await fetchHandler(url), {dataField: 'data'})
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
/**
|
|
@@ -224,12 +225,13 @@ export async function togglePlaylistPrivate(playlistId, is_private)
|
|
|
224
225
|
* Updates a playlists values
|
|
225
226
|
*
|
|
226
227
|
* @param {string|number} playlistId
|
|
227
|
-
* @param {
|
|
228
|
+
* @param {UpdatePlaylistDTO} updateData - An object containing fields to update on the playlist:
|
|
228
229
|
* - `name` (string): The name of the new playlist (required, max 255 characters).
|
|
229
230
|
* - `description` (string): A description of the playlist (optional, max 1000 characters).
|
|
230
|
-
* - `category` (string): The category of the playlist.
|
|
231
|
-
*
|
|
232
|
-
*
|
|
231
|
+
* - `category` (string): The category of the playlist (optional).
|
|
232
|
+
* - `is_private` (boolean): Whether the playlist is private (optional, defaults to false).
|
|
233
|
+
* - `deleted_items` (array): List of playlist item IDs to delete (optional).
|
|
234
|
+
* - `item_order` (array): Updated order of playlist items (ids, not railcontent_ids) (optional).
|
|
233
235
|
*
|
|
234
236
|
* @returns {Promise<object>} - A promise that resolves to the created playlist data and lessons if successful, or an error response if validation fails.
|
|
235
237
|
*
|
|
@@ -242,16 +244,14 @@ export async function togglePlaylistPrivate(playlistId, is_private)
|
|
|
242
244
|
* .then(response => console.log(response.playlist); console.log(response.lessons))
|
|
243
245
|
* .catch(error => console.error('Error updating playlist:', error));
|
|
244
246
|
*/
|
|
245
|
-
export async function updatePlaylist(playlistId,
|
|
246
|
-
name = null, description = null, is_private = null, brand = null, category = null, deleted_items = null, item_order = null
|
|
247
|
-
})
|
|
247
|
+
export async function updatePlaylist(playlistId, updateData)
|
|
248
248
|
{
|
|
249
|
-
const
|
|
249
|
+
const { name, description, category, is_private, item_order, deleted_items } = updateData;
|
|
250
|
+
let data = {
|
|
250
251
|
...name && { name },
|
|
251
|
-
...description && { description },
|
|
252
|
-
...is_private
|
|
253
|
-
...
|
|
254
|
-
...category && { category},
|
|
252
|
+
...'description' in updateData && { description },
|
|
253
|
+
...'is_private' in updateData && { private: is_private || false },
|
|
254
|
+
...'category' in updateData && { category },
|
|
255
255
|
...deleted_items && { deleted_items },
|
|
256
256
|
...item_order && { item_order },
|
|
257
257
|
}
|
|
@@ -344,7 +344,7 @@ export async function duplicatePlaylist(playlistId, playlistData) {
|
|
|
344
344
|
*/
|
|
345
345
|
export async function fetchPlaylist(playlistId) {
|
|
346
346
|
const url = `${BASE_PATH}/v1/user/playlists/${playlistId}`
|
|
347
|
-
return await fetchHandler(url
|
|
347
|
+
return await getNavigateToForPlaylists(await fetchHandler(url))
|
|
348
348
|
}
|
|
349
349
|
|
|
350
350
|
/**
|
package/src/services/content.js
CHANGED
|
@@ -80,6 +80,7 @@ export async function getTabResults(brand, pageName, tabName, {
|
|
|
80
80
|
results = await addContextToContent(getLessonContentRows, brand, pageName, {
|
|
81
81
|
dataField: 'items',
|
|
82
82
|
addNextLesson: true,
|
|
83
|
+
addNavigateTo: true,
|
|
83
84
|
addProgressPercentage: true,
|
|
84
85
|
addProgressStatus: true
|
|
85
86
|
})
|
|
@@ -87,6 +88,7 @@ export async function getTabResults(brand, pageName, tabName, {
|
|
|
87
88
|
let temp = await fetchTabData(brand, pageName, { page, limit, sort, includedFields: mergedIncludedFields, progress: progressValue });
|
|
88
89
|
results = await addContextToContent(() => temp.entity, {
|
|
89
90
|
addNextLesson: true,
|
|
91
|
+
addNavigateTo: true,
|
|
90
92
|
addProgressPercentage: true,
|
|
91
93
|
addProgressStatus: true
|
|
92
94
|
})
|
|
@@ -382,7 +384,7 @@ export async function getRecommendedForYou(brand, rowId = null, {
|
|
|
382
384
|
limit = 10,
|
|
383
385
|
} = {}) {
|
|
384
386
|
const requiredItems = page * limit;
|
|
385
|
-
const data = await recommendations(brand, {limit: requiredItems})
|
|
387
|
+
const data = await recommendations( brand, {limit: requiredItems})
|
|
386
388
|
if (!data || !data.length) {
|
|
387
389
|
return { id: 'recommended', title: 'Recommended For You', items: [] };
|
|
388
390
|
}
|
|
@@ -390,14 +392,11 @@ export async function getRecommendedForYou(brand, rowId = null, {
|
|
|
390
392
|
// Apply pagination before calling fetchByRailContentIds
|
|
391
393
|
const startIndex = (page - 1) * limit;
|
|
392
394
|
const paginatedData = data.slice(startIndex, startIndex + limit);
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
items: contents
|
|
399
|
-
};
|
|
400
|
-
|
|
395
|
+
const contents = await addContextToContent(fetchByRailContentIds, paginatedData, 'tab-data',
|
|
396
|
+
{
|
|
397
|
+
addNextLesson: true,
|
|
398
|
+
addNavigateTo: true,
|
|
399
|
+
})
|
|
401
400
|
if (rowId) {
|
|
402
401
|
return {
|
|
403
402
|
type: TabResponseType.CATALOG,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
getLastInteractedOf,
|
|
3
|
-
getNextLesson,
|
|
2
|
+
getLastInteractedOf, getNavigateTo,
|
|
3
|
+
getNextLesson, getProgressDateByIds,
|
|
4
4
|
getProgressPercentageByIds,
|
|
5
5
|
getProgressStateByIds,
|
|
6
6
|
getResumeTimeSecondsByIds
|
|
@@ -28,6 +28,7 @@ import {fetchLastInteractedChild, fetchLikeCount} from "./railcontent"
|
|
|
28
28
|
* @param options.addIsLiked - add isLikedField
|
|
29
29
|
* @param options.addLikeCount - add likeCount field
|
|
30
30
|
* @param options.addProgressStatus - add progressStatus field
|
|
31
|
+
* @param options.addProgressTimestamp - add progressTimestamp field
|
|
31
32
|
* @param options.addResumeTimeSeconds - add resumeTimeSeconds field
|
|
32
33
|
* @param options.addLastInteractedChild - add lastInteractedChild field. This may be different from nextLesson
|
|
33
34
|
* @param options.addNextLesson - add nextLesson field. For collection type content. each collection has different logic for calculating this data
|
|
@@ -66,9 +67,11 @@ export async function addContextToContent(dataPromise, ...dataArgs)
|
|
|
66
67
|
addIsLiked = false,
|
|
67
68
|
addLikeCount = false,
|
|
68
69
|
addProgressStatus = false,
|
|
70
|
+
addProgressTimestamp = false,
|
|
69
71
|
addResumeTimeSeconds = false,
|
|
70
72
|
addLastInteractedChild = false,
|
|
71
73
|
addNextLesson = false,
|
|
74
|
+
addNavigateTo = false,
|
|
72
75
|
} = options
|
|
73
76
|
|
|
74
77
|
const dataParam = lastArg === options ? dataArgs.slice(0, -1) : dataArgs
|
|
@@ -81,29 +84,63 @@ export async function addContextToContent(dataPromise, ...dataArgs)
|
|
|
81
84
|
|
|
82
85
|
if(ids.length === 0) return false
|
|
83
86
|
|
|
84
|
-
const [
|
|
85
|
-
addProgressPercentage ?
|
|
86
|
-
addProgressStatus ? getProgressStateByIds(ids) : Promise.resolve(null),
|
|
87
|
+
const [progressData, isLikedData, resumeTimeData, lastInteractedChildData, nextLessonData, navigateToData] = await Promise.all([
|
|
88
|
+
addProgressPercentage || addProgressStatus || addProgressTimestamp ? getProgressDateByIds(ids) : Promise.resolve(null),
|
|
87
89
|
addIsLiked ? isContentLikedByIds(ids) : Promise.resolve(null),
|
|
88
90
|
addResumeTimeSeconds ? getResumeTimeSecondsByIds(ids) : Promise.resolve(null),
|
|
89
91
|
addLastInteractedChild ? fetchLastInteractedChild(ids) : Promise.resolve(null),
|
|
90
92
|
addNextLesson ? getNextLesson(items) : Promise.resolve(null),
|
|
93
|
+
addNavigateTo ? getNavigateTo(items) : Promise.resolve(null),
|
|
91
94
|
])
|
|
95
|
+
if (addNextLesson) console.log('AddNextLesson is depreciated in favour of addNavigateTo')
|
|
92
96
|
|
|
93
97
|
const addContext = async (item) => ({
|
|
94
98
|
...item,
|
|
95
|
-
...(addProgressPercentage ? { progressPercentage:
|
|
96
|
-
...(addProgressStatus ? { progressStatus:
|
|
99
|
+
...(addProgressPercentage ? { progressPercentage: progressData?.[item.id]['progress'] } : {}),
|
|
100
|
+
...(addProgressStatus ? { progressStatus: progressData?.[item.id]['status'] } : {}),
|
|
101
|
+
...(addProgressTimestamp ? { progressTimestamp: progressData?.[item.id]['last_update'] } : {}),
|
|
97
102
|
...(addIsLiked ? { isLiked: isLikedData?.[item.id] } : {}),
|
|
98
103
|
...(addLikeCount && ids.length === 1 ? { likeCount: await fetchLikeCount(item.id) } : {}),
|
|
99
104
|
...(addResumeTimeSeconds ? { resumeTime: resumeTimeData?.[item.id] } : {}),
|
|
100
105
|
...(addLastInteractedChild ? { lastInteractedChild: lastInteractedChildData?.[item.id] } : {}),
|
|
101
106
|
...(addNextLesson ? { nextLesson: nextLessonData?.[item.id] } : {}),
|
|
107
|
+
...(addNavigateTo ? { navigateTo: navigateToData?.[item.id] } : {}),
|
|
102
108
|
})
|
|
103
109
|
|
|
104
110
|
return await processItems(data, addContext, dataField, isDataAnArray, dataField_includeParent)
|
|
105
111
|
}
|
|
106
112
|
|
|
113
|
+
export async function getNavigateToForPlaylists(data, {dataField = null} = {} )
|
|
114
|
+
{
|
|
115
|
+
let playlists = extractItemsFromData(data, dataField, false, false)
|
|
116
|
+
let allIds = []
|
|
117
|
+
playlists.forEach((playlist) => allIds = [...allIds, ...playlist.items.map(a => a.content_id)])
|
|
118
|
+
const progressOnItems = await getProgressStateByIds(allIds);
|
|
119
|
+
const addContext = async (playlist) => {
|
|
120
|
+
const allItemsCompleted = playlist.items.every(i => {
|
|
121
|
+
const itemId = i.content_id;
|
|
122
|
+
const progress = progressOnItems[itemId];
|
|
123
|
+
return progress && progress === 'completed';
|
|
124
|
+
});
|
|
125
|
+
let nextItem = playlist.items[0] ?? null;
|
|
126
|
+
if (!allItemsCompleted) {
|
|
127
|
+
const lastItemProgress = progressOnItems[playlist.last_engaged_on];
|
|
128
|
+
const index = playlist.items.findIndex(i => i.content_id === playlist.last_engaged_on);
|
|
129
|
+
if (lastItemProgress === 'completed') {
|
|
130
|
+
nextItem = playlist.items[index + 1] ?? nextItem;
|
|
131
|
+
} else {
|
|
132
|
+
nextItem = playlist.items[index] ?? nextItem;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
playlist.navigateTo = {
|
|
136
|
+
...nextItem,
|
|
137
|
+
playlist_id: playlist.id,
|
|
138
|
+
}
|
|
139
|
+
return playlist
|
|
140
|
+
}
|
|
141
|
+
return await processItems(data, addContext, dataField, false, false,)
|
|
142
|
+
}
|
|
143
|
+
|
|
107
144
|
function extractItemsFromData(data, dataField, isParentArray, includeParent)
|
|
108
145
|
{
|
|
109
146
|
let items = []
|
|
@@ -159,3 +196,4 @@ async function processItems(data, addContext, dataField, isParentArray, includeP
|
|
|
159
196
|
}
|
|
160
197
|
}
|
|
161
198
|
|
|
199
|
+
|
|
@@ -90,6 +90,73 @@ export async function getNextLesson(data)
|
|
|
90
90
|
return nextLessonData
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
export async function getNavigateTo(data)
|
|
94
|
+
{
|
|
95
|
+
let navigateToData = {}
|
|
96
|
+
const twoDepthContentTypes = ['pack'] //TODO add method when we know what it's called
|
|
97
|
+
//TODO add parent hierarchy upwards as well
|
|
98
|
+
// data structure is the same but instead of child{} we use parent{}
|
|
99
|
+
for (const content of data) {
|
|
100
|
+
|
|
101
|
+
//only calculate nextLesson if needed, based on content type
|
|
102
|
+
if (!getNextLessonLessonParentTypes.includes(content.type) || !content.children) {
|
|
103
|
+
navigateToData[content.id] = null
|
|
104
|
+
} else {
|
|
105
|
+
const children = new Map()
|
|
106
|
+
const childrenIds = []
|
|
107
|
+
content.children.forEach(child => {
|
|
108
|
+
childrenIds.push(child.id)
|
|
109
|
+
children.set(child.id, child)
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
// return first child (or grand child) if parent-content is complete or no progress
|
|
113
|
+
const contentState = await getProgressState(content.id)
|
|
114
|
+
if (contentState !== STATE_STARTED) {
|
|
115
|
+
const firstChild = content.children[0]
|
|
116
|
+
let lastInteractedChildNavToData = await getNavigateTo([firstChild])[firstChild.id] ?? null
|
|
117
|
+
navigateToData[content.id] = buildNavigateTo(content.children[0], lastInteractedChildNavToData)
|
|
118
|
+
} else {
|
|
119
|
+
const childrenStates = await getProgressStateByIds(childrenIds)
|
|
120
|
+
const lastInteracted = await getLastInteractedOf(childrenIds)
|
|
121
|
+
const lastInteractedStatus = childrenStates[lastInteracted]
|
|
122
|
+
|
|
123
|
+
if (content.type === 'course' || content.type === 'pack-bundle') {
|
|
124
|
+
if (lastInteractedStatus === STATE_STARTED) {
|
|
125
|
+
navigateToData[content.id] = buildNavigateTo(children.get(lastInteracted))
|
|
126
|
+
} else {
|
|
127
|
+
let incompleteChild = findIncompleteLesson(childrenStates, lastInteracted, content.type)
|
|
128
|
+
navigateToData[content.id] = buildNavigateTo(children.get(incompleteChild))
|
|
129
|
+
}
|
|
130
|
+
} else if (content.type === 'guided-course' || content.type === 'song-tutorial') {
|
|
131
|
+
let incompleteChild = findIncompleteLesson(childrenStates, lastInteracted, content.type)
|
|
132
|
+
navigateToData[content.id] = buildNavigateTo(children.get(incompleteChild))
|
|
133
|
+
} else if (twoDepthContentTypes.includes(content.type)) {
|
|
134
|
+
const firstChildren = content.children ?? []
|
|
135
|
+
const lastInteractedChildId = await getLastInteractedOf(firstChildren.map(child => child.id));
|
|
136
|
+
if (childrenStates[lastInteractedChildId] === STATE_COMPLETED) {
|
|
137
|
+
// 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
|
+
}
|
|
139
|
+
let lastInteractedChildNavToData = await getNavigateTo(firstChildren)
|
|
140
|
+
lastInteractedChildNavToData = lastInteractedChildNavToData[lastInteractedChildId]
|
|
141
|
+
navigateToData[content.id] = buildNavigateTo(children.get(lastInteractedChildId), lastInteractedChildNavToData);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return navigateToData
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildNavigateTo(content, child = null)
|
|
150
|
+
{
|
|
151
|
+
return {
|
|
152
|
+
brand: content.brand,
|
|
153
|
+
thumbnail: content.thumbnail ?? '',
|
|
154
|
+
id: content.id,
|
|
155
|
+
type: content.type,
|
|
156
|
+
child: child,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
93
160
|
/**
|
|
94
161
|
* filter through contents, only keeping the most recent
|
|
95
162
|
* @param {array} contentIds
|