musora-content-services 2.28.5 → 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.
@@ -3,7 +3,6 @@
3
3
  */
4
4
  import { globalConfig } from './config.js'
5
5
  import { fetchJSONHandler } from '../lib/httpHelper.js'
6
- import { convertToTimeZone } from './dateUtils.js';
7
6
 
8
7
  /**
9
8
  * Exported functions that are excluded from index generation.
@@ -285,7 +284,7 @@ async function postDataHandler(url, data) {
285
284
  }
286
285
 
287
286
  async function patchDataHandler_depreciated(url, data) {
288
- throw Error("PATCH verb throws a CORS error on the FEW. Use PATCH instead")
287
+ throw Error('PATCH verb throws a CORS error on the FEW. Use PATCH instead')
289
288
  }
290
289
 
291
290
  async function putDataHandler(url, data) {
@@ -296,8 +295,8 @@ async function deleteDataHandler(url, data) {
296
295
  return fetchHandler(url, 'delete')
297
296
  }
298
297
 
299
- export async function fetchLikeCount(contendId){
300
- const url = `/api/content/v1/content/like_count/${contendId}`
298
+ export async function fetchLikeCount(contendId) {
299
+ const url = `/api/content/v1/content/like_count/${contendId}`
301
300
  return await fetchDataHandler(url)
302
301
  }
303
302
 
@@ -584,40 +583,36 @@ export async function fetchComment(commentId) {
584
583
  }
585
584
 
586
585
  export async function fetchUserPractices(currentVersion = 0, { userId } = {}) {
587
- const params = new URLSearchParams();
588
- if (userId) params.append('user_id', userId);
589
- const query = params.toString() ? `?${params.toString()}` : '';
590
- const url = `/api/user/practices/v1/practices${query}`;
591
- const response = await fetchDataHandler(url, currentVersion);
592
- const { data, version } = response;
593
- const userPractices = data;
594
- if(!userPractices ) {
595
- return { data: { practices: {} }, version };
586
+ const params = new URLSearchParams()
587
+ if (userId) params.append('user_id', userId)
588
+ const query = params.toString() ? `?${params.toString()}` : ''
589
+ const url = `/api/user/practices/v1/practices${query}`
590
+ const response = await fetchDataHandler(url, currentVersion)
591
+ const { data, version } = response
592
+ const userPractices = data
593
+ if (!userPractices) {
594
+ return { data: { practices: {} }, version }
596
595
  }
597
596
 
598
- const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
599
-
600
597
  const formattedPractices = userPractices.reduce((acc, practice) => {
601
- // Convert UTC date to user's local date (still a Date object)
602
- const userTimeZoneDay = convertToTimeZone(practice.day, userTimeZone);
603
- if (!acc[userTimeZoneDay]) {
604
- acc[userTimeZoneDay] = [];
598
+ if (!acc[practice.day]) {
599
+ acc[practice.day] = []
605
600
  }
606
601
 
607
- acc[userTimeZoneDay].push({
602
+ acc[practice.day].push({
608
603
  id: practice.id,
609
604
  duration_seconds: practice.duration_seconds,
610
- });
605
+ })
611
606
 
612
- return acc;
613
- }, {});
607
+ return acc
608
+ }, {})
614
609
 
615
610
  return {
616
611
  data: {
617
612
  practices: formattedPractices,
618
613
  },
619
614
  version,
620
- };
615
+ }
621
616
  }
622
617
 
623
618
  export async function logUserPractice(practiceDetails) {
@@ -628,11 +623,11 @@ export async function fetchUserPracticeMeta(practiceIds, userId = null) {
628
623
  if (practiceIds.length == 0) {
629
624
  return []
630
625
  }
631
- const params = new URLSearchParams();
632
- practiceIds.forEach(id => params.append('practice_ids[]', id));
626
+ const params = new URLSearchParams()
627
+ practiceIds.forEach((id) => params.append('practice_ids[]', id))
633
628
 
634
629
  if (userId !== null) {
635
- params.append('user_id', userId);
630
+ params.append('user_id', userId)
636
631
  }
637
632
  const url = `/api/user/practices/v1/practices?${params.toString()}`
638
633
  return await fetchHandler(url, 'GET', null)
@@ -653,7 +648,6 @@ export async function fetchUserPracticeNotes(date) {
653
648
  return await fetchHandler(url, 'GET', null)
654
649
  }
655
650
 
656
-
657
651
  /**
658
652
  * Get the id and slug of last interacted child. Only valid for certain content types
659
653
  *
@@ -674,8 +668,8 @@ export async function fetchUserPracticeNotes(date) {
674
668
  * }
675
669
  */
676
670
  export async function fetchLastInteractedChild(content_ids) {
677
- const params = new URLSearchParams();
678
- content_ids.forEach(id => params.append('content_ids[]', id));
671
+ const params = new URLSearchParams()
672
+ content_ids.forEach((id) => params.append('content_ids[]', id))
679
673
  const url = `/api/content/v1/user/last_interacted_child?${params.toString()}`
680
674
  return await fetchHandler(url, 'GET', null)
681
675
  }
@@ -708,18 +702,13 @@ export async function fetchLastInteractedChild(content_ids) {
708
702
  * .then(activities => console.log(activities))
709
703
  * .catch(error => console.error(error));
710
704
  */
711
- export async function fetchRecentUserActivities({
712
- page = 1,
713
- limit = 5,
714
- tabName = null
715
- } = {}) {
705
+ export async function fetchRecentUserActivities({ page = 1, limit = 5, tabName = null } = {}) {
716
706
  let pageAndLimit = `?page=${page}&limit=${limit}`
717
707
  let tabParam = tabName ? `&tabName=${tabName}` : ''
718
708
  const url = `/api/user-management-system/v1/activities/all${pageAndLimit}${tabParam}`
719
709
  return await fetchHandler(url, 'GET', null)
720
710
  }
721
711
 
722
-
723
712
  function fetchAbsolute(url, params) {
724
713
  if (globalConfig.sessionConfig.authToken) {
725
714
  params.headers['Authorization'] = `Bearer ${globalConfig.sessionConfig.authToken}`
@@ -12,6 +12,8 @@ import { HttpClient } from '../infrastructure/http/HttpClient'
12
12
  */
13
13
  const excludeFromGeneratedIndex = []
14
14
 
15
+ const baseURL = 'https://recommender.musora.com'
16
+
15
17
  /**
16
18
  * Fetches similar content to the provided content id
17
19
  *
@@ -36,10 +38,7 @@ export async function fetchSimilarItems(content_id, brand, count = 10) {
36
38
  }
37
39
  const url = `/similar_items/`
38
40
  try {
39
- const httpClient = new HttpClient(
40
- globalConfig.recommendationsConfig.baseUrl,
41
- globalConfig.recommendationsConfig.token
42
- )
41
+ const httpClient = new HttpClient(baseURL)
43
42
  const response = await httpClient.post(url, data)
44
43
  // we requested count + 1 then filtered out the extra potential value, so we need slice to the correct size if necessary
45
44
  return response['similar_items'].filter((item) => item !== content_id).slice(0, count)
@@ -75,23 +74,29 @@ export async function rankCategories(brand, categories) {
75
74
  }
76
75
  const url = `/rank_each_list/`
77
76
  try {
78
- const httpClient = new HttpClient(
79
- globalConfig.recommendationsConfig.baseUrl,
80
- globalConfig.recommendationsConfig.token
81
- )
77
+ const httpClient = new HttpClient(baseURL)
82
78
  const response = await httpClient.post(url, data)
83
79
  let rankedCategories = []
84
80
 
85
81
  for (const rankedPlaylist of response['ranked_playlists']) {
86
82
  rankedCategories.push({
87
- 'slug': rankedPlaylist.playlist_id,
88
- 'items': rankedPlaylist.ranked_items
83
+ slug: rankedPlaylist.playlist_id,
84
+ items: rankedPlaylist.ranked_items,
89
85
  })
90
86
  }
91
87
  return rankedCategories
92
88
  } catch (error) {
93
- console.error('Fetch error:', error)
94
- return null
89
+ console.error('RankCategories fetch error:', error)
90
+ const defaultSorting = []
91
+ for (const slug in categories) {
92
+ defaultSorting.push(
93
+ {
94
+ slug: slug,
95
+ items: categories[slug],
96
+ }
97
+ )
98
+ }
99
+ return defaultSorting
95
100
  }
96
101
  }
97
102
 
@@ -117,15 +122,12 @@ export async function rankItems(brand, content_ids) {
117
122
  }
118
123
  const url = `/rank_items/`
119
124
  try {
120
- const httpClient = new HttpClient(
121
- globalConfig.recommendationsConfig.baseUrl,
122
- globalConfig.recommendationsConfig.token
123
- )
125
+ const httpClient = new HttpClient(baseURL)
124
126
  const response = await httpClient.post(url, data)
125
127
  return response['ranked_content_ids']
126
128
  } catch (error) {
127
- console.error('Fetch error:', error)
128
- return null
129
+ console.error('rankItems fetch error:', error)
130
+ return content_ids
129
131
  }
130
132
  }
131
133
 
@@ -16,6 +16,7 @@ import {
16
16
  showsTypes,
17
17
  getNewReleasesTypes,
18
18
  coachLessonsTypes,
19
+ getFieldsForContentTypeWithFilteredChildren,
19
20
  getChildFieldsForContentType,
20
21
  SONG_TYPES,
21
22
  } from '../contentTypeConfig.js'
@@ -75,7 +76,7 @@ export async function fetchSongById(documentId) {
75
76
  */
76
77
  export async function fetchLeaving(brand, { pageNumber = 1, contentPerPage = 20 } = {}) {
77
78
  const today = new Date()
78
- const isoDateOnly = today.toISOString().split('T')[0]
79
+ const isoDateOnly = getDateOnly(today)
79
80
  const filterString = `brand == '${brand}' && quarter_removed > '${isoDateOnly}'`
80
81
  const startEndOrder = getQueryFromPage(pageNumber, contentPerPage)
81
82
  const sortOrder = {
@@ -102,7 +103,7 @@ export async function fetchLeaving(brand, { pageNumber = 1, contentPerPage = 20
102
103
  */
103
104
  export async function fetchReturning(brand, { pageNumber = 1, contentPerPage = 20 } = {}) {
104
105
  const today = new Date()
105
- const isoDateOnly = today.toISOString().split('T')[0]
106
+ const isoDateOnly = getDateOnly(today)
106
107
  const filterString = `brand == '${brand}' && quarter_published >= '${isoDateOnly}'`
107
108
  const startEndOrder = getQueryFromPage(pageNumber, contentPerPage)
108
109
  const sortOrder = {
@@ -159,41 +160,6 @@ function getQueryFromPage(pageNumber, contentPerPage) {
159
160
  return result
160
161
  }
161
162
 
162
- /**
163
- * returns array of next and previous quarter dates as strings
164
- *
165
- * @returns {string[]}
166
- */
167
- function getNextAndPreviousQuarterDates() {
168
- const january = 1
169
- const april = 4
170
- const july = 7
171
- const october = 10
172
- const month = new Date().getMonth()
173
- let year = new Date().getFullYear()
174
- let nextQuarter = ''
175
- let prevQuarter = ''
176
- if (month < april) {
177
- nextQuarter = `${year}-0${april}-01`
178
- prevQuarter = `${year}-0${january}-01`
179
- } else if (month < july) {
180
- nextQuarter = `${year}-0${july}-01`
181
- prevQuarter = `${year}-0${april}-01`
182
- } else if (month < october) {
183
- nextQuarter = `${year}-${october}-01`
184
- prevQuarter = `${year}-0${july}-01`
185
- } else {
186
- prevQuarter = `${year}-${october}-01`
187
- year++
188
- nextQuarter = `${year}-0${january}-01`
189
- }
190
-
191
- let result = []
192
- result['next'] = nextQuarter
193
- result['previous'] = prevQuarter
194
- return result
195
- }
196
-
197
163
  /**
198
164
  * Fetch all artists with lessons available for a specific brand.
199
165
  *
@@ -350,8 +316,8 @@ export async function fetchNewReleases(
350
316
  const start = (page - 1) * limit
351
317
  const end = start + limit
352
318
  const sortOrder = getSortOrder(sort, brand)
353
- const nextQuarter = getNextAndPreviousQuarterDates()['next']
354
- const filter = `_type in ${typesString} && brand == '${brand}' && status == 'published' && show_in_new_feed == true && (!defined(quarter_published) || quarter_published != '${nextQuarter}')`
319
+ const now = getDateOnly()
320
+ const filter = `_type in ${typesString} && brand == '${brand}' && (status == 'published' && show_in_new_feed == true && published_on <= '${now}')`
355
321
  const fields = `
356
322
  "id": railcontent_id,
357
323
  title,
@@ -367,12 +333,7 @@ export async function fetchNewReleases(
367
333
  web_url_path,
368
334
  "permission_id": permission[]->railcontent_id,
369
335
  `
370
- const filterParams = { allowsPullSongsContent: false }
371
- const query = await buildQuery(filter, filterParams, fields, {
372
- sortOrder: sortOrder,
373
- start,
374
- end: end,
375
- })
336
+ const query = buildRawQuery(filter, fields, {sortOrder: sortOrder, start, end: end})
376
337
  return fetchSanity(query, true)
377
338
  }
378
339
 
@@ -477,12 +438,12 @@ export async function fetchScheduledReleases(brand, { page = 1, limit = 10 }) {
477
438
  * .catch(error => console.error(error));
478
439
  */
479
440
  export async function fetchByRailContentId(id, contentType) {
480
- const fields = getFieldsForContentType(contentType)
481
- const childFields = getChildFieldsForContentType(contentType)
441
+ const fields = await getFieldsForContentTypeWithFilteredChildren(contentType)
442
+ const lessonFields = getChildFieldsForContentType(contentType)
482
443
  const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
483
444
  const entityFieldsString = ` ${fields}
484
445
  'child_count': coalesce(count(child[${childrenFilter}]->), 0) ,
485
- "lessons": child[${childrenFilter}]->{${childFields}},
446
+ "lessons": child[${childrenFilter}]->{${lessonFields}},
486
447
  'length_in_seconds': coalesce(
487
448
  math::sum(
488
449
  select(
@@ -519,17 +480,19 @@ export async function fetchByRailContentIds(ids, contentType = undefined, brand
519
480
  if (!ids?.length) {
520
481
  return []
521
482
  }
483
+ ids = [...new Set(ids.filter(item => item !== null && item !== undefined))];
522
484
  const idsString = ids.join(',')
523
485
  const brandFilter = brand ? ` && brand == "${brand}"` : ''
524
- const now = getSanityDate(new Date())
486
+ const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`, {pullFutureContent: true}).buildFilter()
487
+ const fields = await getFieldsForContentTypeWithFilteredChildren(contentType, true)
525
488
  const query = `*[
526
489
  railcontent_id in [${idsString}]${brandFilter}
527
490
  ]{
528
- ${getFieldsForContentType(contentType)}
491
+ ${fields}
492
+ 'lesson_count': coalesce(count(*[${lessonCountFilter}]), 0),
529
493
  live_event_start_time,
530
494
  live_event_end_time,
531
495
  }`
532
-
533
496
  const customPostProcess = (results) => {
534
497
  const now = getSanityDate(new Date(), false);
535
498
  const liveProcess = (result) => {
@@ -555,7 +518,7 @@ export async function fetchByRailContentIds(ids, contentType = undefined, brand
555
518
  }
556
519
 
557
520
  // Sort results to match the order of the input IDs
558
- const sortedResults = results.sort(sortFuction)
521
+ const sortedResults = results?.sort(sortFuction) ?? null
559
522
 
560
523
  return sortedResults
561
524
  }
@@ -565,6 +528,7 @@ export async function fetchContentRows(brand, pageName, contentRowSlug)
565
528
  if (pageName === 'lessons') pageName = 'lesson'
566
529
  if (pageName === 'songs') pageName = 'song'
567
530
  const rowString = contentRowSlug ? ` && slug.current == "${contentRowSlug.toLowerCase()}"` : ''
531
+ const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`, {pullFutureContent: true}).buildFilter()
568
532
  const childFilter = await new FilterBuilder('', {isChildrenFilter: true}).buildFilter()
569
533
  const query = `*[_type == 'recommended-content-row' && brand == '${brand}' && type == '${pageName}'${rowString}]{
570
534
  brand,
@@ -573,6 +537,7 @@ export async function fetchContentRows(brand, pageName, contentRowSlug)
573
537
  'content': content[${childFilter}]->{
574
538
  'children': child[${childFilter}]->{ 'id': railcontent_id, 'children': child[${childFilter}]->{'id': railcontent_id}, },
575
539
  ${getFieldsForContentType('tab-data')}
540
+ 'lesson_count': coalesce(count(*[${lessonCountFilter}]), 0),
576
541
  },
577
542
  }`
578
543
  return fetchSanity(query, true)
@@ -1172,19 +1137,17 @@ export async function fetchLessonContent(railContentId) {
1172
1137
  const query = await buildQuery(`railcontent_id == ${railContentId}`, filterParams, fields, {
1173
1138
  isSingle: true,
1174
1139
  })
1175
- console.log('query', query)
1176
1140
  const chapterProcess = (result) => {
1177
1141
  const now = getSanityDate(new Date(), false)
1178
1142
  if (result.live_event_start_time && result.live_event_end_time) {
1179
1143
  result.isLive = result.live_event_start_time <= now && result.live_event_end_time >= now
1180
1144
  }
1181
1145
  const chapters = result.chapters ?? []
1182
- if (chapters.length == 0) return result
1146
+ if (chapters.length === 0) return result
1183
1147
  result.chapters = chapters.map((chapter, index) => ({
1184
1148
  ...chapter,
1185
1149
  chapter_thumbnail_url: `https://musora-web-platform.s3.amazonaws.com/chapters/${result.brand}/Chapter${index + 1}.jpg`,
1186
1150
  }))
1187
- console.log('result', result)
1188
1151
  return result
1189
1152
  }
1190
1153
 
@@ -1474,7 +1437,7 @@ export async function fetchLiveEvent(brand, forcedContentId = null) {
1474
1437
  */
1475
1438
  export async function fetchPackData(id) {
1476
1439
  const query = `*[railcontent_id == ${id}]{
1477
- ${getFieldsForContentType('pack')}
1440
+ ${await getFieldsForContentTypeWithFilteredChildren('pack')}
1478
1441
  } [0...1]`
1479
1442
  return fetchSanity(query, false)
1480
1443
  }
@@ -2013,6 +1976,10 @@ function getSanityDate(date, roundToHourForCaching = true) {
2013
1976
  return date.toISOString()
2014
1977
  }
2015
1978
 
1979
+ function getDateOnly(date = new Date()) {
1980
+ return date.toISOString().split('T')[0]
1981
+ }
1982
+
2016
1983
  const merge = (a, b, predicate = (a, b) => a === b) => {
2017
1984
  const c = [...a] // copy to avoid side effects
2018
1985
  // add all items from B to copy C if they're not already present
@@ -2230,11 +2197,12 @@ export async function fetchTabData(
2230
2197
 
2231
2198
  filter = `brand == "${brand}" ${includedFieldsFilter} ${progressFilter}`
2232
2199
  const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
2200
+ const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`).buildFilter()
2233
2201
  entityFieldsString =
2234
2202
  ` ${fieldsString}
2235
2203
  'children': child[${childrenFilter}]->{'id': railcontent_id},
2236
2204
  'isLive': live_event_start_time <= "${now}" && live_event_end_time >= "${now}",
2237
- 'lesson_count': coalesce(count(child[${childrenFilter}]->), 0),
2205
+ 'lesson_count': coalesce(count(*[${lessonCountFilter}]), 0),
2238
2206
  'length_in_seconds': coalesce(
2239
2207
  math::sum(
2240
2208
  select(
@@ -2305,6 +2273,7 @@ export async function fetchScheduledAndNewReleases(
2305
2273
  "id": railcontent_id,
2306
2274
  title,
2307
2275
  "image": thumbnail.asset->url,
2276
+ "thumbnail": thumbnail.asset->url,
2308
2277
  ${artistOrInstructorName()},
2309
2278
  "artists": instructor[]->name,
2310
2279
  difficulty,
@@ -2327,6 +2296,7 @@ export async function fetchShows(brand, type, sort = 'sort') {
2327
2296
 
2328
2297
  const query = await buildQuery(filter, filterParams, getFieldsForContentType(type), {
2329
2298
  sortOrder: sortOrder,
2299
+ end: 100, // Adrian: added for homepage progress rows, this should be handled gracefully
2330
2300
  })
2331
2301
  return fetchSanity(query, true)
2332
2302
  }
@@ -29,20 +29,12 @@
29
29
  * @property {string} authToken - The bearer authorization token.
30
30
  */
31
31
 
32
- /**
33
- * @typedef {object} RecommendationsConfig - Configuration for recommendation services.
34
- *
35
- * @property {string} token - The token for authenticating recommendation requests.
36
- * @property {string} baseUrl - The url for the recommendation server.
37
- */
38
-
39
32
  /**
40
33
  * @typedef {object} Config
41
34
  *
42
35
  * @property {SanityConfig} sanityConfig
43
36
  * @property {RailcontentConfig} railcontentConfig - DEPRECATED use sessionConfig and baseUrl instead.
44
37
  * @property {SessionConfig} sessionConfig
45
- * @property {RecommendationsConfig} recommendationsConfig
46
38
  * @property {string} baseUrl - The url for the environment.
47
39
  * @property {Object} localStorage - Cache to use for localStorage
48
40
  * @property {boolean} isMA - Variable that tells if the library is used by MA or FEW
@@ -39,11 +39,5 @@ export async function otherStats(userId = globalConfig.sessionConfig.userId) {
39
39
  */
40
40
  export async function deleteProfilePicture() {
41
41
  const url = `${baseUrl}/v1/users/profile_picture`
42
- const response = await fetchHandler(url, 'DELETE')
43
-
44
- if (!response.ok) {
45
- const problemDetails = await response.json()
46
- console.log('Error deleting profile picture:', problemDetails.detail)
47
- throw new Error(`Delete failed: ${problemDetails.detail}`)
48
- }
42
+ await fetchHandler(url, 'DELETE')
49
43
  }