musora-content-services 2.69.1 → 2.70.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md 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.70.1](https://github.com/railroadmedia/musora-content-services/compare/v2.70.0...v2.70.1) (2025-11-05)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * update video field for method objects ([#541](https://github.com/railroadmedia/musora-content-services/issues/541)) ([a2120f6](https://github.com/railroadmedia/musora-content-services/commit/a2120f63bfa99d9b9ae96712e879161ad4add441))
11
+
12
+ ## [2.70.0](https://github.com/railroadmedia/musora-content-services/compare/v2.67.2...v2.70.0) (2025-11-05)
13
+
14
+
15
+ ### Features
16
+
17
+ * add fields for learning path v2 ([#537](https://github.com/railroadmedia/musora-content-services/issues/537)) ([b2bcbd2](https://github.com/railroadmedia/musora-content-services/commit/b2bcbd2306afa730e5c61d6ee34fe082212dfdbb))
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * Add is_content and page_type to all content ([#507](https://github.com/railroadmedia/musora-content-services/issues/507)) ([174706b](https://github.com/railroadmedia/musora-content-services/commit/174706b736540b9d24ceaee11539df9203d2d135))
23
+
5
24
  ### [2.69.1](https://github.com/railroadmedia/musora-content-services/compare/v2.69.0...v2.69.1) (2025-11-04)
6
25
 
7
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.69.1",
3
+ "version": "2.70.1",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -39,6 +39,19 @@ class SortingOptions {
39
39
  ]
40
40
  }
41
41
 
42
+ export class LengthFilterOptions {
43
+ static UpTo7 = { value: '<420', name: 'Up to 7 Minutes' }
44
+ static From7To15 = { value: '420-900', name: '7 to 15 Minutes' }
45
+ static From15To30 = { value: '901-1800', name: '15 to 30 Minutes' }
46
+ static More30 = { value: '>1801', name: '30+ Minutes' }
47
+ static AllOptions = [
48
+ this.UpTo7.name,
49
+ this.From7To15.name,
50
+ this.From15To30.name,
51
+ this.More30.name,
52
+ ]
53
+ }
54
+
42
55
  export class Tabs {
43
56
  static ForYou = { name: 'For You', short_name: 'For You' }
44
57
  static Individuals = { name: 'Individuals', short_name: 'Individuals', value: 'type,individuals', cardType: 'big' }
@@ -82,6 +95,7 @@ const commonMetadata = {
82
95
  name: 'Lessons',
83
96
  filterOptions: {
84
97
  difficulty: DIFFICULTY_STRINGS,
98
+ length: LengthFilterOptions.AllOptions,
85
99
  style: ['Country/Folk', 'Funk/Disco', 'Hard Rock/Metal', 'Hip-Hop/Rap/EDM', 'Holiday/Soundtrack', 'Jazz/Blues', 'Latin/World', 'Pop/Rock', 'R&B/Soul', 'Worship/Gospel'],
86
100
  type: LESSON_TYPE_FILTER,
87
101
  progress: PROGRESS_NAMES,
@@ -157,6 +171,7 @@ const contentMetadata = {
157
171
  name: 'Lessons',
158
172
  filterOptions: {
159
173
  difficulty: DIFFICULTY_STRINGS,
174
+ length: LengthFilterOptions.AllOptions,
160
175
  style: ['Country/Folk', 'Funk/Disco', 'Hard Rock/Metal', 'Hip-Hop/Rap/EDM', 'Holiday/Soundtrack', 'Jazz/Blues', 'Latin/World', 'Pop/Rock', 'R&B/Soul', 'Worship/Gospel'],
161
176
  type: LESSON_TYPE_FILTER,
162
177
  progress: PROGRESS_NAMES,
@@ -182,6 +197,7 @@ const contentMetadata = {
182
197
  name: 'Lessons',
183
198
  filterOptions: {
184
199
  difficulty: DIFFICULTY_STRINGS,
200
+ length: LengthFilterOptions.AllOptions,
185
201
  style: ['Classical', 'Country/Folk', 'Funk/Disco', 'Hip-Hop/Rap/EDM', 'Holiday/Soundtrack', 'Jazz/Blues', 'Latin/World', 'Pop/Rock', 'R&B/Soul', 'Worship/Gospel'],
186
202
  type: LESSON_TYPE_FILTER,
187
203
  progress: PROGRESS_NAMES,
@@ -1,5 +1,5 @@
1
1
  //import {AWSUrl, CloudFrontURl} from "./services/config";
2
- import {Tabs} from "./contentMetaData.js";
2
+ import {LengthFilterOptions, Tabs} from "./contentMetaData.js";
3
3
  import {FilterBuilder} from "./filterBuilder.js";
4
4
 
5
5
  export const AWSUrl = 'https://s3.us-east-1.amazonaws.com/musora-web-platform'
@@ -418,6 +418,7 @@ export let contentTypeConfig = {
418
418
  },
419
419
  'learning-path-v2': {
420
420
  fields: [
421
+ 'intro_video',
421
422
  'total_skills',
422
423
  `"resource": ${resourcesField}`,
423
424
  `"badge": *[
@@ -635,11 +636,7 @@ export let contentTypeConfig = {
635
636
  "description": ${descriptionField},
636
637
  "thumbnail": thumbnail.asset->url,
637
638
  length_in_seconds,
638
- "intro_video": intro_video->{
639
- external_id,
640
- hlsManifestUrl,
641
- video_playback_endpoints
642
- },
639
+ intro_video,
643
640
  child[]->{
644
641
  ${DEFAULT_FIELDS.join(',')}
645
642
  }
@@ -655,16 +652,8 @@ export function getIntroVideoFields() {
655
652
  `"description": ${descriptionField}`,
656
653
  `"thumbnail": thumbnail.asset->url`,
657
654
  "length_in_seconds",
658
- `video_desktop {
659
- external_id,
660
- hlsManifestUrl,
661
- video_playback_endpoints
662
- }`,
663
- `video_mobile {
664
- external_id,
665
- hlsManifestUrl,
666
- video_playback_endpoints
667
- }`
655
+ "video_desktop",
656
+ "video_mobile",
668
657
  ];
669
658
  }
670
659
 
@@ -823,136 +812,142 @@ export function getFieldsForContentType(contentType, asQueryString = true) {
823
812
  }
824
813
 
825
814
  /**
826
- * Takes the included fields array and returns a string that can be used in a groq query.
827
- * @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',
828
- * 'genre,rock']
829
- * @returns {string} - A string that can be used in a groq query
815
+ * Helper function to create type conditions from content type arrays
830
816
  */
831
- export function filtersToGroq(filters, selectedFilters = [], pageName = '') {
832
- if (!filters) {
833
- filters = []
834
- }
817
+ function createTypeConditions(lessonTypes) {
818
+ if (!lessonTypes || lessonTypes.length === 0) return ''
819
+ const conditions = lessonTypes.map(type => `_type == '${type}'`).join(' || ')
820
+ return conditions ? `(${conditions})` : ''
821
+ }
822
+
823
+ /**
824
+ * Filter handler registry - maps filter keys to their handler functions
825
+ */
826
+ const filterHandlers = {
827
+ style: (value) => `"${value}" in genre[]->name`,
828
+
829
+ difficulty: (value) => {
830
+ if (value === 'Introductory') {
831
+ return `(difficulty_string == "Novice" || difficulty_string == "Introductory")`
832
+ }
833
+ return `difficulty_string == "${value}"`
834
+ },
835
+
836
+ tab: (value, pageName) => {
837
+ const valueLower = value.toLowerCase()
838
+ const tabMappings = {
839
+ [Tabs.Individuals.name.toLowerCase()]: individualLessonsTypes,
840
+ [Tabs.Collections.name.toLowerCase()]: collectionLessonTypes,
841
+ [Tabs.Tutorials.name.toLowerCase()]: tutorialsLessonTypes,
842
+ [Tabs.Transcriptions.name.toLowerCase()]: transcriptionsLessonTypes,
843
+ [Tabs.PlayAlongs.name.toLowerCase()]: playAlongLessonTypes,
844
+ [Tabs.JamTracks.name.toLowerCase()]: jamTrackLessonTypes,
845
+ [Tabs.ExploreAll.name.toLowerCase()]: filterTypes[pageName] || [],
846
+ [Tabs.RecentAll.name.toLowerCase()]: recentTypes[pageName] || [],
847
+ [Tabs.SingleLessons.name.toLowerCase()]: individualLessonsTypes,
848
+ [Tabs.Courses.name.toLowerCase()]: coursesLessonTypes,
849
+ [Tabs.SkillPacks.name.toLowerCase()]: skillLessonTypes,
850
+ [Tabs.Entertainment.name.toLowerCase()]: entertainmentLessonTypes,
851
+ }
852
+
853
+ const lessonTypes = tabMappings[valueLower]
854
+ if (lessonTypes) {
855
+ return createTypeConditions(lessonTypes)
856
+ }
857
+
858
+ return `_type == "${value}"`
859
+ },
860
+
861
+ type: (value) => {
862
+ const typeKey = value.toLowerCase()
863
+ const lessonTypes = lessonTypesMapping[typeKey]
864
+
865
+ if (lessonTypes) {
866
+ return createTypeConditions(lessonTypes)
867
+ }
868
+
869
+ return `_type == "${value}"`
870
+ },
871
+
872
+ length: (value) => {
873
+ // Find the matching length option by name
874
+ const lengthOption = Object.values(LengthFilterOptions)
875
+ .find(opt => typeof opt === 'object' && opt.name === value)
835
876
 
836
- //Account for multiple railcontent id's
837
- let multipleIdFilters = ''
838
- filters.forEach((item) => {
839
- if (item.includes('railcontent_id in')) {
840
- filters.pop(item)
841
- multipleIdFilters += ` && ${item} `
877
+ if (!lengthOption) return ''
878
+
879
+ const optionValue = lengthOption.value
880
+
881
+ // Parse the value format: '<420', '420-900', '>1801'
882
+ if (optionValue.startsWith('<')) {
883
+ const max = parseInt(optionValue.substring(1), 10)
884
+ return `(length_in_seconds < ${max})`
885
+ }
886
+
887
+ if (optionValue.startsWith('>')) {
888
+ const min = parseInt(optionValue.substring(1), 10)
889
+ return `(length_in_seconds > ${min})`
890
+ }
891
+
892
+ if (optionValue.includes('-')) {
893
+ const [min, max] = optionValue.split('-').map(Number)
894
+ return `(length_in_seconds >= ${min} && length_in_seconds <= ${max})`
842
895
  }
843
- })
844
896
 
845
- //Group All Other filters
846
- const groupedFilters = groupFilters(filters)
897
+ return ''
898
+ },
899
+
900
+ pageName: () => '', // pageName is meta, doesn't generate a query
901
+ }
902
+
903
+ /**
904
+ * Takes the included fields array and returns a string that can be used in the groq query.
905
+ * @param {Array<string>} filters - An array of strings that represent applied filters.
906
+ * Format: ['difficulty,Intermediate', 'genre,rock']
907
+ * @param {Array<string>} selectedFilters - Filters to exclude from processing
908
+ * @param {string} pageName - Current page name for context-specific filtering
909
+ * @returns {string} - A GROQ query filter string
910
+ */
911
+ export function filtersToGroq(filters = [], selectedFilters = [], pageName = '') {
912
+ // Handle railcontent_id filters separately (they use different syntax)
913
+ const railcontentIdFilters = filters
914
+ .filter(item => item.includes('railcontent_id in'))
915
+ .map(item => ` && ${item}`)
916
+ .join('')
917
+
918
+ // Remove railcontent_id filters from main processing
919
+ const regularFilters = filters.filter(item => !item.includes('railcontent_id in'))
920
+
921
+ // Group filters by key
922
+ const groupedFilters = groupFilters(regularFilters)
847
923
 
848
- //Format groupFilter itemsss
924
+ // Process each filter group
849
925
  const filterClauses = Object.entries(groupedFilters)
850
926
  .map(([key, values]) => {
927
+ // Skip empty filters
851
928
  if (!key || values.length === 0) return ''
929
+
930
+ // Handle boolean flags (is_*)
852
931
  if (key.startsWith('is_')) {
853
932
  return `&& ${key} == true`
854
933
  }
855
- // Filter out values that exist in selectedFilters
934
+
935
+ // Skip if in selectedFilters
936
+ if (selectedFilters.includes(key)) {
937
+ return ''
938
+ }
939
+
940
+ // Process each value with the appropriate handler
856
941
  const joinedValues = values
857
- .map((value) => {
858
- if (key === 'bpm' && !selectedFilters.includes('bpm')) {
859
- if (value.includes('-')) {
860
- const [min, max] = value.split('-').map(Number)
861
- return `(bpm > ${min} && bpm < ${max})`
862
- } else if (value.includes('+')) {
863
- const min = parseInt(value, 10)
864
- return `(bpm > ${min})`
865
- } else {
866
- return `bpm == ${value}`
867
- }
868
- } else if (
869
- ['creativity', 'essential', 'focus', 'genre', 'lifestyle', 'theory', 'topic'].includes(
870
- key
871
- ) &&
872
- !selectedFilters.includes(key)
873
- ) {
874
- return `"${value}" in ${key}[]->name`
875
- } else if (
876
- ['style'].includes(
877
- key
878
- ) &&
879
- !selectedFilters.includes(key)
880
- ) {
881
- return `"${value}" in genre[]->name`
882
- } else if (key === 'gear' && !selectedFilters.includes('gear')) {
883
- return `gear match "${value}"`
884
- } else if (key === 'instrumentless' && !selectedFilters.includes(key)) {
885
- if (value === 'Full Song Only') {
886
- return `(!instrumentless || instrumentless == null)`
887
- } else if (value === 'Instrument Removed') {
888
- return `instrumentless`
889
- } else {
890
- return `instrumentless == ${value}`
891
- }
892
- } else if (key === 'difficulty' && !selectedFilters.includes(key)) {
893
- if(value === 'Introductory'){
894
- return `(difficulty_string == "Novice" || difficulty_string == "Introductory" )`
895
- }
896
- return `difficulty_string == "${value}"`
897
- } else if (key === 'tab' && !selectedFilters.includes(key)) {
898
- if(value.toLowerCase() === Tabs.SingleLessons.name.toLowerCase()){
899
- const conditions = individualLessonsTypes.map(lessonType => `_type == '${lessonType}'`).join(' || ');
900
- return ` (${conditions})`;
901
- } else if(value.toLowerCase() === Tabs.Courses.name.toLowerCase()){
902
- const conditions = coursesLessonTypes.map(lessonType => `_type == '${lessonType}'`).join(' || ');
903
- return ` (${conditions})`;
904
- } else if(value.toLowerCase() === Tabs.Entertainment.name.toLowerCase()){
905
- const conditions = entertainmentLessonTypes.map(lessonType => `_type == '${lessonType}'`).join(' || ');
906
- return ` (${conditions})`;
907
- } else if(value.toLowerCase() === Tabs.Tutorials.name.toLowerCase()){
908
- const conditions = tutorialsLessonTypes.map(lessonType => `_type == '${lessonType}'`).join(' || ');
909
- return ` (${conditions})`;
910
- } else if(value.toLowerCase() === Tabs.Transcriptions.name.toLowerCase()){
911
- const conditions = transcriptionsLessonTypes.map(lessonType => `_type == '${lessonType}'`).join(' || ');
912
- return ` (${conditions})`;
913
- } else if(value.toLowerCase() === Tabs.PlayAlongs.name.toLowerCase()){
914
- const conditions = playAlongLessonTypes.map(lessonType => `_type == '${lessonType}'`).join(' || ');
915
- return ` (${conditions})`;
916
- } else if(value.toLowerCase() === Tabs.JamTracks.name.toLowerCase()){
917
- const conditions = jamTrackLessonTypes.map(lessonType => `_type == '${lessonType}'`).join(' || ');
918
- return ` (${conditions})`;
919
- } else if(value.toLowerCase() === Tabs.ExploreAll.name.toLowerCase()){
920
- var allLessons = filterTypes[pageName] || [];
921
- const conditions = allLessons.map(lessonType => `_type == '${lessonType}'`).join(' || ');
922
- if (conditions === "") return '';
923
- return ` (${conditions})`;
924
- }else if(value.toLowerCase() === Tabs.RecentAll.name.toLowerCase()){
925
- var allLessons = recentTypes[pageName] || [];
926
- const conditions = allLessons.map(lessonType => `_type == '${lessonType}'`).join(' || ');
927
- if (conditions === "") return '';
928
- return ` (${conditions})`;
929
- }
930
- return `_type == "${value}"`
931
- } else if (key === 'type' && !selectedFilters.includes(key)) {
932
- const typeKey = value.toLowerCase();
933
- const lessonTypes = lessonTypesMapping[typeKey];
934
- if (lessonTypes) {
935
- const conditions = lessonTypes.map(
936
- (lessonType) => `_type == '${lessonType}'`
937
- ).join(' || ');
938
- return ` (${conditions})`;
939
- }
940
- return `_type == "${value}"`;
941
- } else if (key === 'length_in_seconds') {
942
- if (value.includes('-')) {
943
- const [min, max] = value.split('-').map(Number)
944
- return `(${key} > ${min} && ${key} < ${max})`
945
- } else if (value.includes('+')) {
946
- const min = parseInt(value, 10)
947
- return `(${key} > ${min})`
948
- } else {
949
- return `${key} == ${value}`
950
- }
951
- } else if (key === 'pageName') {
952
- return ` `
953
- } else if (!selectedFilters.includes(key)) {
954
- return ` ${key} == ${/^\d+$/.test(value) ? value : `"$${value}"`}`
942
+ .map(value => {
943
+ const handler = filterHandlers[key]
944
+
945
+ if (handler) {
946
+ return handler(value, pageName)
955
947
  }
948
+
949
+ // Default handler for unknown filters
950
+ return `${key} == ${/^\d+$/.test(value) ? value : `"${value}"`}`
956
951
  })
957
952
  .filter(Boolean)
958
953
  .join(' || ')
@@ -963,10 +958,14 @@ export function filtersToGroq(filters, selectedFilters = [], pageName = '') {
963
958
  .filter(Boolean)
964
959
  .join(' ')
965
960
 
966
- //Return
967
- return `${multipleIdFilters} ${filterClauses}`
961
+ return `${railcontentIdFilters} ${filterClauses}`.trim()
968
962
  }
969
963
 
964
+ /**
965
+ * Groups filters by category
966
+ * @param {Array<string>} filters - Array of 'key,value' strings
967
+ * @returns {Object} - Object with keys as categories and values as arrays
968
+ */
970
969
  function groupFilters(filters) {
971
970
  if (filters.length === 0) return {}
972
971
 
@@ -14,7 +14,6 @@ import {
14
14
  fetchLeaving, fetchScheduledAndNewReleases, fetchContentRows
15
15
  } from './sanity.js'
16
16
  import {TabResponseType, Tabs, capitalizeFirstLetter} from '../contentMetaData.js'
17
- import {fetchHandler} from "./railcontent";
18
17
  import {recommendations, rankCategories, rankItems} from "./recommendations";
19
18
  import {addContextToContent} from "./contentAggregator.js";
20
19
 
@@ -90,7 +89,7 @@ export async function getTabResults(brand, pageName, tabName, {
90
89
  let temp = await fetchTabData(brand, pageName, { page, limit, sort, includedFields: mergedIncludedFields, progress: progressValue });
91
90
 
92
91
  const [ranking, contextResults] = await Promise.all([
93
- sort === 'recommended' ? rankItems(brand, temp.entity.map(e => e.railcontent_id)) : [],
92
+ sort === 'recommended' ? rankItems(brand, temp.entity.map(e => e.id)) : [],
94
93
  addContextToContent(() => temp.entity, {
95
94
  addNextLesson: true,
96
95
  addNavigateTo: true,
@@ -100,8 +99,8 @@ export async function getTabResults(brand, pageName, tabName, {
100
99
  ]);
101
100
 
102
101
  results = ranking.length === 0 ? contextResults : contextResults.sort((a, b) => {
103
- const indexA = ranking.indexOf(a.railcontent_id);
104
- const indexB = ranking.indexOf(b.railcontent_id);
102
+ const indexA = ranking.indexOf(a.id);
103
+ const indexB = ranking.indexOf(b.id);
105
104
  return (indexA === -1 ? Infinity : indexA) - (indexB === -1 ? Infinity : indexB);
106
105
  })
107
106
  }
@@ -436,7 +435,7 @@ export async function getRecommendedForYou(brand, rowId = null, {
436
435
  export async function getLegacyMethods(brand) {
437
436
 
438
437
  // TODO: Replace with real data from Sanity when available with permissions
439
-
438
+
440
439
  return [
441
440
  {
442
441
  id: 1,
@@ -451,4 +450,4 @@ export async function getLegacyMethods(brand) {
451
450
  child_count: 12,
452
451
  },
453
452
  ]
454
- }
453
+ }
File without changes
@@ -2187,7 +2187,7 @@ export async function fetchTabData(
2187
2187
  let entityFieldsString = ''
2188
2188
  let filter = ''
2189
2189
 
2190
- filter = `brand == "${brand}" ${includedFieldsFilter} ${progressFilter}`
2190
+ filter = `brand == "${brand}" && (defined(railcontent_id)) ${includedFieldsFilter} ${progressFilter}`
2191
2191
  const childrenFilter = await new FilterBuilder(``, { isChildrenFilter: true }).buildFilter()
2192
2192
  const childrenFields = await getChildFieldsForContentType('tab-data')
2193
2193
  const lessonCountFilter = await new FilterBuilder(`_id in ^.child[]._ref`).buildFilter()