musora-content-services 2.20.0 → 2.21.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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(rg:*)"
5
+ ],
6
+ "deny": []
7
+ }
8
+ }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
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.21.0](https://github.com/railroadmedia/musora-content-services/compare/v2.20.0...v2.21.0) (2025-07-12)
6
+
7
+
8
+ ### Features
9
+
10
+ * **BEH-572:** ai recommender row categories ([#337](https://github.com/railroadmedia/musora-content-services/issues/337)) ([d1984d2](https://github.com/railroadmedia/musora-content-services/commit/d1984d28371fa2d4f22d8ddb50fdcfd952399b46))
11
+
5
12
  ## [2.20.0](https://github.com/railroadmedia/musora-content-services/compare/v2.19.2...v2.20.0) (2025-07-11)
6
13
 
7
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.20.0",
3
+ "version": "2.21.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -24,7 +24,6 @@ export const DEFAULT_FIELDS = [
24
24
  "'url': web_url_path",
25
25
  'published_on',
26
26
  "'type': _type",
27
- 'progress_percent',
28
27
  "'length_in_seconds' : coalesce(length_in_seconds, soundslice[0].soundslice_length_in_second)",
29
28
  'brand',
30
29
  "'genre': genre[]->name",
@@ -139,7 +138,7 @@ export const singleLessonTypes = ['quick-tips', 'rudiment', 'coach-lessons'];
139
138
  export const practiceAlongsLessonTypes = ['workout', 'boot-camp','challenges'];
140
139
  export const performancesLessonTypes = ['performance','solo','drum-fest-international-2022'];
141
140
  export const documentariesLessonTypes = ['tama','sonor','history-of-electronic-drums','paiste-cymbals'];
142
- export const liveArchivesLessonTypes = ['podcast', 'coach-stream', 'question-and-answer', 'live-streams'];
141
+ export const liveArchivesLessonTypes = ['podcast', 'coach-stream', 'question-and-answer', 'live-streams', 'live'];
143
142
  export const studentArchivesLessonTypes = ['student-review', 'student-focus','student-collaboration'];
144
143
  export const tutorialsLessonTypes = ['song-tutorial'];
145
144
  export const transcriptionsLessonTypes = ['song'];
@@ -209,7 +208,8 @@ export const filterTypes = {
209
208
  export const recentTypes = {
210
209
  lessons: [...individualLessonsTypes],
211
210
  songs: [...tutorialsLessonTypes, ...transcriptionsLessonTypes, ...playAlongLessonTypes],
212
- home: [...individualLessonsTypes, ...tutorialsLessonTypes, ...transcriptionsLessonTypes, ...playAlongLessonTypes]
211
+ home: [...individualLessonsTypes, ...tutorialsLessonTypes, ...transcriptionsLessonTypes, ...playAlongLessonTypes,
212
+ 'guided-course', 'learning-path', 'live']
213
213
  }
214
214
 
215
215
  export let contentTypeConfig = {
package/src/index.d.ts CHANGED
@@ -189,6 +189,7 @@ import {
189
189
  fetchCoachLessons,
190
190
  fetchComingSoon,
191
191
  fetchCommentModContentData,
192
+ fetchContentRows,
192
193
  fetchFoundation,
193
194
  fetchGenreLessons,
194
195
  fetchHierarchy,
@@ -364,6 +365,7 @@ declare module 'musora-content-services' {
364
365
  fetchContentInProgress,
365
366
  fetchContentPageUserData,
366
367
  fetchContentProgress,
368
+ fetchContentRows,
367
369
  fetchEnrollmentPageMetadata,
368
370
  fetchFoundation,
369
371
  fetchGenreLessons,
package/src/index.js CHANGED
@@ -189,6 +189,7 @@ import {
189
189
  fetchCoachLessons,
190
190
  fetchComingSoon,
191
191
  fetchCommentModContentData,
192
+ fetchContentRows,
192
193
  fetchFoundation,
193
194
  fetchGenreLessons,
194
195
  fetchHierarchy,
@@ -363,6 +364,7 @@ export {
363
364
  fetchContentInProgress,
364
365
  fetchContentPageUserData,
365
366
  fetchContentProgress,
367
+ fetchContentRows,
366
368
  fetchEnrollmentPageMetadata,
367
369
  fetchFoundation,
368
370
  fetchGenreLessons,
@@ -11,11 +11,12 @@ import {
11
11
  fetchUpcomingEvents,
12
12
  fetchScheduledReleases,
13
13
  fetchReturning,
14
- fetchLeaving, fetchScheduledAndNewReleases
14
+ fetchLeaving, fetchScheduledAndNewReleases, fetchContentRows
15
15
  } from './sanity.js'
16
16
  import {TabResponseType, Tabs, capitalizeFirstLetter} from '../contentMetaData.js'
17
17
  import {fetchHandler} from "./railcontent";
18
- import {recommendations} from "./recommendations";
18
+ import {recommendations, rankCategories} from "./recommendations";
19
+ import {addContextToContent} from "./contentAggregator.js";
19
20
 
20
21
 
21
22
  export async function getLessonContentRows (brand='drumeo', pageName = 'lessons') {
@@ -149,11 +150,11 @@ export async function getRecent(brand, pageName, tabName = 'all', {
149
150
  }
150
151
 
151
152
  /**
152
- * Fetches content rows for a given brand and page with optional filtering by content row id.
153
+ * Fetches content rows for a given brand and page with optional filtering by content row slug.
153
154
  *
154
155
  * @param {string} brand - The brand for which to fetch content rows.
155
156
  * @param {string} pageName - The page name (e.g., 'lessons', 'songs', 'challenges').
156
- * @param {string} [contentRowId] - The specific content row ID to fetch.
157
+ * @param {string|null} contentRowSlug - The specific content row ID to fetch.
157
158
  * @param {Object} params - Parameters for pagination.
158
159
  * @param {number} [params.page=1] - The page number for pagination.
159
160
  * @param {number} [params.limit=10] - The maximum number of content items per row.
@@ -168,32 +169,48 @@ export async function getRecent(brand, pageName, tabName = 'all', {
168
169
  * .then(content => console.log(content))
169
170
  * .catch(error => console.error(error));
170
171
  */
171
- export async function getContentRows(brand, pageName, contentRowId , {
172
+ export async function getContentRows(brand, pageName, contentRowSlug = null, {
172
173
  page = 1,
173
- limit = 10,
174
+ limit = 10
174
175
  } = {}) {
175
- const contentRow = contentRowId ? `&content_row_id=${contentRowId}` : ''
176
- const url = `/api/content/v1/rows?brand=${brand}&page_name=${pageName}${contentRow}&page=${page}&limit=${limit}`;
177
- const contentRows = await fetchHandler(url, 'get', null) || [];
178
- const results = await Promise.all(
179
- contentRows.map(async (row) => {
180
- if (row.content.length === 0){
181
- return { id: row.id, title: row.title, items: [] }
182
- }
183
- const data = await fetchByRailContentIds(row.content)
184
- return { id: row.id, title: row.title, items: data }
185
- })
186
- )
176
+ const sanityData = await addContextToContent(fetchContentRows, brand, pageName, contentRowSlug, {
177
+ dataField: 'content',
178
+ iterateDataFieldOnEachArrayElement: true,
179
+ addProgressStatus: true,
180
+ addProgressPercentage: true,
181
+ addNextLesson: true
182
+ })
183
+ if (!sanityData) {
184
+ return []
185
+ }
186
+ let contentMap = {}
187
+ let recData = {}
188
+ let slugNameMap = {}
189
+ for (const category of sanityData) {
190
+ recData[category.slug] = category.content.map(item => item.railcontent_id)
191
+ for (const content of category.content) {
192
+ contentMap[content.railcontent_id] = content
193
+ }
194
+ slugNameMap[category.slug] = category.name
195
+ }
196
+ const start = (page - 1) * limit
197
+ const end = start + limit
198
+ const sortedData = await rankCategories(brand, recData)
199
+ let finalData = []
200
+ for (const category of sortedData) {
201
+ finalData.push( {
202
+ id: category.slug,
203
+ title: slugNameMap[category.slug],
204
+ items: category.items.slice(start, end).map(id => contentMap[id])})
205
+ }
187
206
 
188
- if (contentRowId) {
189
- return {
207
+ return contentRowSlug ?
208
+ {
190
209
  type: TabResponseType.CATALOG,
191
- data: results[0].items,
210
+ data: finalData[0].items,
192
211
  meta: {}
193
- };
194
- }
195
-
196
- return results
212
+ }
213
+ : finalData
197
214
  }
198
215
 
199
216
  /**
@@ -16,6 +16,7 @@ export async function addContextToContent(dataPromise, ...dataArgs)
16
16
 
17
17
  const {
18
18
  dataField = null,
19
+ iterateDataFieldOnEachArrayElement = false,
19
20
  addProgressPercentage = false,
20
21
  addIsLiked = false,
21
22
  addLikeCount = false,
@@ -34,8 +35,14 @@ export async function addContextToContent(dataPromise, ...dataArgs)
34
35
  let items = []
35
36
  let dataMap = []
36
37
 
37
- if (dataField && data?.[dataField]) {
38
- items = data[dataField];
38
+ if (dataField && (data?.[dataField] || iterateDataFieldOnEachArrayElement)) {
39
+ if (iterateDataFieldOnEachArrayElement && Array.isArray(data)) {
40
+ for(const parent of data) {
41
+ ids = [...ids, ...parent[dataField].map(item => item?.id).filter(Boolean)]
42
+ }
43
+ } else {
44
+ ids = data[dataField].map(item => item?.id).filter(Boolean);
45
+ }
39
46
  } else if (Array.isArray(data)) {
40
47
  items = data;
41
48
  } else if (data?.id) {
@@ -85,9 +92,17 @@ export async function addContextToContent(dataPromise, ...dataArgs)
85
92
  }
86
93
 
87
94
  if (dataField) {
88
- data[dataField] = Array.isArray(data[dataField])
95
+ if (iterateDataFieldOnEachArrayElement) {
96
+ for(let parent of data) {
97
+ parent[dataField] = Array.isArray(parent[dataField])
98
+ ? await Promise.all(parent[dataField].map(addContext))
99
+ : await addContext(parent[dataField])
100
+ }
101
+ } else {
102
+ data[dataField] = Array.isArray(data[dataField])
89
103
  ? await Promise.all(data[dataField].map(addContext))
90
104
  : await addContext(data[dataField])
105
+ }
91
106
  return data
92
107
  } else {
93
108
  return Array.isArray(data)
File without changes
@@ -80,11 +80,14 @@ export async function rankCategories(brand, categories) {
80
80
  globalConfig.recommendationsConfig.token
81
81
  )
82
82
  const response = await httpClient.post(url, data)
83
- let rankedCategories = {}
84
- response['ranked_playlists'].forEach(
85
- (category) =>
86
- (rankedCategories[category['playlist_id']] = categories[category['playlist_id']])
87
- )
83
+ let rankedCategories = []
84
+
85
+ for (const rankedPlaylist of response['ranked_playlists']) {
86
+ rankedCategories.push({
87
+ 'slug': rankedPlaylist.playlist_id,
88
+ 'items': rankedPlaylist.ranked_items
89
+ })
90
+ }
88
91
  return rankedCategories
89
92
  } catch (error) {
90
93
  console.error('Fetch error:', error)
@@ -540,6 +540,21 @@ export async function fetchByRailContentIds(ids, contentType = undefined, brand
540
540
  return sortedResults
541
541
  }
542
542
 
543
+ export async function fetchContentRows(brand, pageName, contentRowSlug)
544
+ {
545
+ if (pageName === 'lessons') pageName = 'lesson'
546
+ if (pageName === 'songs') pageName = 'song'
547
+ const rowString = contentRowSlug ? ` && slug.current == "${contentRowSlug.toLowerCase()}"` : ''
548
+ return fetchSanity(`*[_type == 'recommended-content-row' && brand == '${brand}' && type == '${pageName}'${rowString}]{
549
+ brand,
550
+ name,
551
+ 'slug': slug.current,
552
+ 'content': content[]->{ ${getFieldsForContentType()} }
553
+ }`, true)
554
+ }
555
+
556
+
557
+
543
558
  /**
544
559
  * Fetch all content for a specific brand and type with pagination, search, and grouping options.
545
560
  * @param {string} brand - The brand for which to fetch content.