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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.28.6",
3
+ "version": "2.30.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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
- `"id": railcontent_id`,
39
- `title`,
40
- `"image": thumbnail.asset->url`,
41
- `"instructors": instructor[]->name`,
42
- `length_in_seconds`,
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-along': playAlongLessonTypes,
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: [...tutorialsLessonTypes, ...transcriptionsLessonTypes, ...playAlongLessonTypes],
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',
@@ -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 {Object} updateData - An object containing fields to update on the playlist:
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
- * + * - `deleted_items` (array): List of playlist item IDs to delete.
232
- * + * - `item_order` (array): Updated order of playlist items (ids, not railcontent_ids).
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 data = {
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 !== null && { private: is_private},
253
- ...brand && { brand },
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, 'GET')
347
+ return await getNavigateToForPlaylists(await fetchHandler(url))
348
348
  }
349
349
 
350
350
  /**
@@ -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
- const contents = await fetchByRailContentIds(paginatedData);
395
- const result = {
396
- id: 'recommended',
397
- title: 'Recommended For You',
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 [progressPercentageData, progressStatusData, isLikedData, resumeTimeData, lastInteractedChildData, nextLessonData] = await Promise.all([
85
- addProgressPercentage ? getProgressPercentageByIds(ids) : Promise.resolve(null),
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: progressPercentageData?.[item.id] } : {}),
96
- ...(addProgressStatus ? { progressStatus: progressStatusData?.[item.id] } : {}),
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