musora-content-services 2.122.6 → 2.123.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,34 @@
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.123.0](https://github.com/railroadmedia/musora-content-services/compare/v2.122.7...v2.123.0) (2026-01-28)
6
+
7
+
8
+ ### Features
9
+
10
+ * better user impersonation ([#731](https://github.com/railroadmedia/musora-content-services/issues/731)) ([768022d](https://github.com/railroadmedia/musora-content-services/commit/768022df839b2c2e206d02985c82075b0d4de008))
11
+ * **T3PS-1537:** Playbass Recommendations V2 ([#744](https://github.com/railroadmedia/musora-content-services/issues/744)) ([b88ffbd](https://github.com/railroadmedia/musora-content-services/commit/b88ffbd85a9982371e108d9f23190a9217cf29d0))
12
+ * **TP-1080:** ignore resume time until past 10s window ([#749](https://github.com/railroadmedia/musora-content-services/issues/749)) ([c22f4c2](https://github.com/railroadmedia/musora-content-services/commit/c22f4c201ec60dbf9475eebbcee199dfea967bbf))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * makes methodIntroComplete function send formatted date matching getDailySession ([#725](https://github.com/railroadmedia/musora-content-services/issues/725)) ([6ac47a0](https://github.com/railroadmedia/musora-content-services/commit/6ac47a04dfa2ea4a0d355409b4be8fa743af1b48))
18
+
19
+ ### [2.122.7](https://github.com/railroadmedia/musora-content-services/compare/v2.122.3...v2.122.7) (2026-01-28)
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * add artist name to practices ([aba6c46](https://github.com/railroadmedia/musora-content-services/commit/aba6c46e03afd6635e74050170cbec5e3b1c9a63))
25
+ * add artist name to recent activity data ([78f0e47](https://github.com/railroadmedia/musora-content-services/commit/78f0e4783d575db30aa6667752ded4366e85b05a))
26
+ * filters out null field ([#742](https://github.com/railroadmedia/musora-content-services/issues/742)) ([cab0b58](https://github.com/railroadmedia/musora-content-services/commit/cab0b584a8f40b7404b869730783aea51a058d35))
27
+ * melon data user isolation ([#717](https://github.com/railroadmedia/musora-content-services/issues/717)) ([6893c3c](https://github.com/railroadmedia/musora-content-services/commit/6893c3c644e1eefcfeeb6439b460a46d853616d6))
28
+ * **T3PS-1586:** onboarding recommendation pinning ([#737](https://github.com/railroadmedia/musora-content-services/issues/737)) ([280305b](https://github.com/railroadmedia/musora-content-services/commit/280305b7706ba171752cb54e6d2ee504093b11d1))
29
+ * **T3PS-2003:** Display Shows like Individuals on the progress cards and progress areas ([5a61d7d](https://github.com/railroadmedia/musora-content-services/commit/5a61d7dfaf5fbed2cc8bdca751c12783acbe2b66))
30
+ * **T3PS-2004:** Play Your Favorite Songs on /method/ Shows Unreleased Content ([b7c32de](https://github.com/railroadmedia/musora-content-services/commit/b7c32deeffe8140969d2e11d853d255b0e184f30))
31
+ * **T3PS-2007:** getPracticeSessions for a given date fails if the content doesn’t have a thumbnail ([d3bb457](https://github.com/railroadmedia/musora-content-services/commit/d3bb45760634f8e8f8f8114859d258da5d3909e6))
32
+
5
33
  ### [2.122.6](https://github.com/railroadmedia/musora-content-services/compare/v2.122.5...v2.122.6) (2026-01-28)
6
34
 
7
35
 
package/CLAUDE.md CHANGED
@@ -328,7 +328,7 @@ import { ContentLike, ContentProgress, Practice, PracticeDayNote } from 'musora-
328
328
  // - DurabilityProvider: no-op (storage always available on mobile)
329
329
  // - TabsProvider: no-op (single "tab" on mobile)
330
330
 
331
- const manager = new SyncManager(context, db)
331
+ const manager = new SyncManager(userScope, context, db)
332
332
  manager.registerStrategies(
333
333
  ContentLike, ContentProgress, Practice, PracticeDayNote],
334
334
  [initialStrategy, onlineStrategy, activityStrategy, hourlyPollingStrategy]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.122.6",
3
+ "version": "2.123.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -4,6 +4,13 @@
4
4
  const PROGRESS_NAMES = ['All', 'In Progress', 'Completed', 'Not Started']
5
5
  const DIFFICULTY_STRINGS = ['Introductory', 'Beginner', 'Intermediate', 'Advanced', 'Expert']
6
6
 
7
+ export const CONTENT_STATUSES = {
8
+ PUBLISHED_ONLY: ['published'],
9
+ ADMIN_ALL: ['draft', 'scheduled', 'published', 'archived', 'unlisted'],
10
+ PUBLIC_WITH_SCHEDULED: ['published', 'scheduled'],
11
+ DRAFT_ONLY: ['draft'],
12
+ }
13
+
7
14
  const LESSON_TYPE_FILTER = [
8
15
  {
9
16
  title: 'Single Lessons',
@@ -49,6 +56,7 @@ export class LengthFilterOptions {
49
56
 
50
57
  export class Tabs {
51
58
  static ForYou = { name: 'For You', short_name: 'For You', value: 'tab,for you' }
59
+ static PlaybassAll = { name: 'All', short_name: 'All', value: 'tab,for you' }
52
60
  static Individuals = { name: 'Individuals', short_name: 'Individuals', value: 'type,individuals', cardType: 'big' }
53
61
  static Collections = { name: 'Collections', short_name: 'Collections', value: 'type,collections', cardType: 'big' }
54
62
  static ExploreAll = { name: 'Explore All', short_name: 'Explore All', value: 'tab,explore all', icon: 'icon-filters', cardType: 'big'}
@@ -323,6 +331,15 @@ const contentMetadata = {
323
331
  },
324
332
  playbass: {
325
333
  'songs-types': ['Tutorials', 'Tabs', 'Play-Alongs', 'Jam Tracks'],
334
+ lessons: {
335
+ tabs: [
336
+ Tabs.PlaybassAll,
337
+ Tabs.SingleLessons,
338
+ Tabs.Courses,
339
+ Tabs.SkillPacks,
340
+ Tabs.ExploreAll,
341
+ ]
342
+ },
326
343
  },
327
344
  singeo: {
328
345
  'songs-types': ['Tutorials', 'Sheet Music', 'Play-Alongs', 'Jam Tracks'],
@@ -238,7 +238,7 @@ export const showsLessonTypes = [
238
238
  'performance',
239
239
  ]
240
240
  export const entertainmentLessonTypes = ['special', 'documentary-lesson', ...showsLessonTypes]
241
- export const collectionLessonTypes = [...coursesLessonTypes, ...showsLessonTypes]
241
+ export const collectionLessonTypes = [...coursesLessonTypes]
242
242
 
243
243
  export const lessonTypesMapping = {
244
244
  lessons: singleLessonTypes,
@@ -350,6 +350,7 @@ export const recentTypes = {
350
350
  ...skillLessonTypes,
351
351
  ...transcriptionsLessonTypes,
352
352
  ...playAlongLessonTypes,
353
+ ...showsLessonTypes,
353
354
  'guided-course',
354
355
  'learning-path-v2',
355
356
  'live',
package/src/index.d.ts CHANGED
@@ -211,7 +211,9 @@ import {
211
211
 
212
212
  import {
213
213
  getProgressRows,
214
+ getUserPinProgressKey,
214
215
  pinProgressRow,
216
+ setUserPinnedProgressRow,
215
217
  unpinProgressRow
216
218
  } from './services/progress-row/base.js';
217
219
 
@@ -634,6 +636,7 @@ declare module 'musora-content-services' {
634
636
  getUpgradePrice,
635
637
  getUserData,
636
638
  getUserMonthlyStats,
639
+ getUserPinProgressKey,
637
640
  getUserSignature,
638
641
  getUserWeeklyStats,
639
642
  getWeekNumber,
@@ -702,6 +705,7 @@ declare module 'musora-content-services' {
702
705
  sendAccountSetupEmail,
703
706
  sendPasswordResetEmail,
704
707
  setStudentViewForUser,
708
+ setUserPinnedProgressRow,
705
709
  setUserSignature,
706
710
  setupAccount,
707
711
  startLearningPath,
package/src/index.js CHANGED
@@ -215,7 +215,9 @@ import {
215
215
 
216
216
  import {
217
217
  getProgressRows,
218
+ getUserPinProgressKey,
218
219
  pinProgressRow,
220
+ setUserPinnedProgressRow,
219
221
  unpinProgressRow
220
222
  } from './services/progress-row/base.js';
221
223
 
@@ -633,6 +635,7 @@ export {
633
635
  getUpgradePrice,
634
636
  getUserData,
635
637
  getUserMonthlyStats,
638
+ getUserPinProgressKey,
636
639
  getUserSignature,
637
640
  getUserWeeklyStats,
638
641
  getWeekNumber,
@@ -701,6 +704,7 @@ export {
701
704
  sendAccountSetupEmail,
702
705
  sendPasswordResetEmail,
703
706
  setStudentViewForUser,
707
+ setUserPinnedProgressRow,
704
708
  setUserSignature,
705
709
  setupAccount,
706
710
  startLearningPath,
@@ -442,14 +442,10 @@ export async function completeMethodIntroVideo(
442
442
  return response
443
443
  }
444
444
 
445
- async function methodIntroVideoCompleteActions(
446
- brand: string,
447
- learningPathId: number,
448
- userDate: Date
449
- ) {
450
- const stringDate = userDate.toISOString().split('T')[0]
445
+ async function methodIntroVideoCompleteActions(brand: string, learningPathId: number, userDate: Date) {
446
+ const dateWithTimezone = formatLocalDateTime(userDate)
451
447
  const url: string = `${LEARNING_PATHS_PATH}/method-intro-video-complete-actions`
452
- const body = { brand: brand, learningPathId: learningPathId, userDate: stringDate }
448
+ const body = { brand: brand, learningPathId: learningPathId, userDate: dateWithTimezone }
453
449
  return (await POST(url, body)) as DailySessionResponse
454
450
  }
455
451
 
@@ -471,10 +471,10 @@ export async function getRecommendedForYou(brand, rowId = null, {
471
471
  } = {}) {
472
472
  const requiredItems = page * limit;
473
473
  const data = await recommendations( brand, {limit: requiredItems})
474
+ const title = brand === 'playbass' ? "You Might Like" : "Recommended For You"
474
475
  if (!data || !data.length) {
475
- return { id: 'recommended', title: 'Recommended For You', items: [] };
476
+ return { id: 'recommended', title: title, items: [] };
476
477
  }
477
-
478
478
  // Apply pagination before calling fetchByRailContentIds
479
479
  const startIndex = (page - 1) * limit;
480
480
  const paginatedData = data.slice(startIndex, startIndex + limit);
@@ -491,7 +491,7 @@ export async function getRecommendedForYou(brand, rowId = null, {
491
491
  };
492
492
  }
493
493
 
494
- return { id: 'recommended', title: 'Recommended For You', items: contents }
494
+ return { id: 'recommended', title: title, items: contents }
495
495
  }
496
496
 
497
497
 
@@ -14,6 +14,8 @@ export interface UserPermissions {
14
14
  permissions: string[]
15
15
  /** Whether the user is an admin */
16
16
  isAdmin: boolean
17
+ /** Whether the user is a moderator */
18
+ isModerator: boolean
17
19
  /** Whether the user has basic membership */
18
20
  isABasicMember: boolean
19
21
  /** User's access level (v2 - for future use) */
@@ -108,4 +110,14 @@ export abstract class PermissionsAdapter {
108
110
  isAdmin(userPermissions: UserPermissions): boolean {
109
111
  return userPermissions?.isAdmin ?? false
110
112
  }
113
+
114
+ /**
115
+ * Check if user is a moderator.
116
+ *
117
+ * @param userPermissions - The user's permissions
118
+ * @returns True if user is moderator
119
+ */
120
+ isModerator(userPermissions: UserPermissions): boolean {
121
+ return userPermissions?.isModerator ?? false
122
+ }
111
123
  }
@@ -58,7 +58,7 @@ function generateContentPromises(contents) {
58
58
  const allRecentTypeSet = new Set(Object.values(recentTypes).flat())
59
59
  contents.forEach((content) => {
60
60
  const type = content.type
61
- if (!allRecentTypeSet.has(type) && !showsLessonTypes.includes(type)) return
61
+ if (!allRecentTypeSet.has(type)) return
62
62
  let childHasParent = Array.isArray(content.parent_content_data) && content.parent_content_data.length > 0
63
63
  if (!childHasParent) {
64
64
  promises.push(processContentItem(content))
@@ -100,28 +100,6 @@ export async function processContentItem(content) {
100
100
  }
101
101
  }
102
102
 
103
- if (contentType === 'show') {
104
- const shows = await fetchShows(content.brand, content.type)
105
- const showIds = shows.map((item) => item.id)
106
- const progressOnItems = await getProgressStateByIds(showIds)
107
- const completedShows = content.completed_children
108
- const progressTimestamp = content.progressTimestamp
109
- const wasPinned = content.pinned ?? false
110
- if (content.progressStatus === 'completed') {
111
- // this could be handled more gracefully if there was a parent content type for shows
112
- // Update Dec 3rd. We updated almost everything to the DocumentaryType :D, but there's still a few
113
- const nextByProgress = findIncompleteLesson(progressOnItems, content.id, content.type)
114
- content = shows.find((lesson) => lesson.id === nextByProgress)
115
- content.completed_children = completedShows
116
- content.progressTimestamp = progressTimestamp
117
- content.pinned = wasPinned
118
- }
119
- content.child_count = shows.length
120
- content.progressPercentage = Math.round((completedShows / shows.length) * 100)
121
- if (completedShows === shows.length) {
122
- ctaText = 'Revisit Show'
123
- }
124
- }
125
103
  return {
126
104
  id: content.id,
127
105
  progressType: 'content',
@@ -138,7 +116,7 @@ export async function processContentItem(content) {
138
116
  subtitle:
139
117
  collectionLessonTypes.includes(content.type) || content.lesson_count > 1
140
118
  ? `${content.completed_children} of ${content.lesson_count ?? content.child_count} Lessons Complete`
141
- : contentType === 'lesson' && isLive === false
119
+ : (contentType === 'lesson' || contentType === 'show') && isLive === false
142
120
  ? `${content.progressPercentage}% Complete`
143
121
  : `${content.difficulty_string} • ${content.artist_name}`,
144
122
  },
@@ -166,7 +144,7 @@ function getDefaultCTATextForContent(content, contentType) {
166
144
  contentType === 'jam track'
167
145
  )
168
146
  ctaText = 'Replay Song'
169
- if (contentType === 'lesson') ctaText = 'Revisit Lesson'
147
+ if (contentType === 'lesson' || contentType === 'show') ctaText = 'Revisit Lesson'
170
148
  if (contentType === 'song tutorial' || collectionLessonTypes.includes(content.type))
171
149
  ctaText = 'Revisit Lessons'
172
150
  if (contentType === 'course-collection') ctaText = 'View Lessons'
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { globalConfig } from './config.js'
6
6
  import { GET, HttpClient } from '../infrastructure/http/HttpClient.ts'
7
+ import { fetchByRailContentIds } from './sanity.js'
7
8
 
8
9
  /**
9
10
  * Exported functions that are excluded from index generation.
@@ -31,20 +32,32 @@ export async function fetchSimilarItems(content_id, brand, count = 10) {
31
32
  if (!content_id) {
32
33
  return []
33
34
  }
34
- content_id = parseInt(content_id)
35
- const data = {
36
- brand: brand,
37
- content_ids: content_id,
38
- num_similar: count + 1,
39
- }
40
- const url = `/similar_items/`
41
- try {
42
- const response = await recommenderClient.post(url, data)
43
- return response['similar_items'].filter((item) => item !== content_id).slice(0, count)
44
- } catch (error) {
45
- console.error('Fetch error:', error)
46
- return null
35
+ if (brand === 'playbass') {
36
+ // V2 launch customization for playbass
37
+ const content = (await fetchByRailContentIds([content_id], 'tab-data'))[0] ?? []
38
+ if (!content) {
39
+ return []
40
+ }
41
+ const section = content.page_type === 'song' ? 'song' : ''
42
+ const recs = await recommendations('playbass', {section: section})
43
+ return recs.slice(0, count)
44
+ } else {
45
+ content_id = parseInt(content_id)
46
+ const data = {
47
+ brand: brand,
48
+ content_ids: content_id,
49
+ num_similar: count + 1,
50
+ }
51
+ const url = `/similar_items/`
52
+ try {
53
+ const response = await recommenderClient.post(url, data)
54
+ return response['similar_items'].filter((item) => item !== content_id).slice(0, count)
55
+ } catch (error) {
56
+ console.error('Fetch error:', error)
57
+ return null
58
+ }
47
59
  }
60
+
48
61
  }
49
62
 
50
63
  /**
@@ -35,7 +35,7 @@ import {
35
35
  liveFields,
36
36
  } from '../contentTypeConfig.js'
37
37
  import { fetchSimilarItems, recommendations } from './recommendations.js'
38
- import { getSongType, processMetadata, ALWAYS_VISIBLE_TABS } from '../contentMetaData.js'
38
+ import { getSongType, processMetadata, ALWAYS_VISIBLE_TABS, CONTENT_STATUSES } from '../contentMetaData.js'
39
39
  import { GET } from '../infrastructure/http/HttpClient.ts'
40
40
 
41
41
  import { globalConfig } from './config.js'
@@ -115,7 +115,7 @@ export async function fetchLeaving(brand, { pageNumber = 1, contentPerPage = 20
115
115
  }
116
116
  const query = await buildQuery(
117
117
  filterString,
118
- { pullFutureContent: false, availableContentStatuses: ['published'] },
118
+ { pullFutureContent: false, availableContentStatuses: CONTENT_STATUSES.PUBLISHED_ONLY },
119
119
  getFieldsForContentType('leaving'),
120
120
  sortOrder
121
121
  )
@@ -142,7 +142,7 @@ export async function fetchReturning(brand, { pageNumber = 1, contentPerPage = 2
142
142
  }
143
143
  const query = await buildQuery(
144
144
  filterString,
145
- { pullFutureContent: true, availableContentStatuses: ['draft'] },
145
+ { pullFutureContent: true, availableContentStatuses: CONTENT_STATUSES.DRAFT_ONLY },
146
146
  getFieldsForContentType('returning'),
147
147
  sortOrder
148
148
  )
@@ -615,6 +615,7 @@ export async function fetchAll(
615
615
  useDefaultFields = true,
616
616
  customFields = [],
617
617
  progress = 'all',
618
+ onlyPublished = true
618
619
  } = {}
619
620
  ) {
620
621
  let config = contentTypeConfig[type] ?? {}
@@ -663,6 +664,9 @@ export async function fetchAll(
663
664
  if (type == 'instructor') {
664
665
  customFilter = '&& coach_card_image != null'
665
666
  }
667
+ if (onlyPublished) {
668
+ customFilter = ' && status == "published" '
669
+ }
666
670
  // Determine the group by clause
667
671
  let query = ''
668
672
  let entityFieldsString = ''
@@ -1907,8 +1911,18 @@ export async function fetchTabData(
1907
1911
  ),
1908
1912
  length_in_seconds
1909
1913
  ),`
1914
+
1915
+ // Check if user is admin to determine available content statuses
1916
+ const adapter = getPermissionsAdapter()
1917
+ const userData = await adapter.fetchUserPermissions()
1918
+ const isAdminORModerator = adapter.isAdmin(userData) || adapter.isModerator(userData)
1919
+
1910
1920
  const filterWithRestrictions = await new FilterBuilder(filter, {
1911
1921
  showMembershipRestrictedContent: true,
1922
+ availableContentStatuses: isAdminORModerator
1923
+ ? CONTENT_STATUSES.ADMIN_ALL
1924
+ : CONTENT_STATUSES.PUBLISHED_ONLY,
1925
+ pullFutureContent: isAdminORModerator ? true : false,
1912
1926
  }).buildFilter()
1913
1927
  query = buildEntityAndTotalQuery(filterWithRestrictions, entityFieldsString, {
1914
1928
  sortOrder: sortOrder,
@@ -154,7 +154,9 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
154
154
  r.progress_percent = progressPct
155
155
 
156
156
  if (typeof resumeTime != 'undefined') {
157
- r.resume_time_seconds = Math.floor(resumeTime)
157
+ if (resumeTime >= 10 || r.resume_time_seconds !== null) {
158
+ r.resume_time_seconds = Math.floor(resumeTime)
159
+ }
158
160
  }
159
161
 
160
162
  if (!fromLearningPath) {
@@ -7,6 +7,7 @@ export type SyncResolution = {
7
7
  tuplesForUpdate: [BaseModel, SyncEntry][]
8
8
  tuplesForRestore: [BaseModel, SyncEntry][]
9
9
  idsForDestroy: RecordId[]
10
+ recordsForSynced: BaseModel[]
10
11
  }
11
12
 
12
13
  export type SyncResolverComparator<T extends BaseModel = BaseModel> = (serverEntry: SyncEntryNonDeleted<T>, localModel: T) => 'SERVER' | 'LOCAL'
@@ -24,7 +25,8 @@ export default class SyncResolver {
24
25
  entriesForCreate: [],
25
26
  tuplesForUpdate: [],
26
27
  tuplesForRestore: [],
27
- idsForDestroy: []
28
+ idsForDestroy: [],
29
+ recordsForSynced: []
28
30
  }
29
31
  }
30
32
 
@@ -58,6 +60,9 @@ export default class SyncResolver {
58
60
  } else if (this.comparator(server as SyncEntryNonDeleted<BaseModel>, local) !== 'LOCAL') {
59
61
  // local is older, so update it with server's
60
62
  this.resolution.tuplesForUpdate.push([local, server])
63
+ } else {
64
+ // server is older - can happen with clock skew - just mark as synced
65
+ this.resolution.recordsForSynced.push(local)
61
66
  }
62
67
  }
63
68
 
@@ -69,6 +74,9 @@ export default class SyncResolver {
69
74
  } else if (this.comparator(server as SyncEntryNonDeleted<BaseModel>, local) !== 'LOCAL') {
70
75
  // local is older, so update it with server's
71
76
  this.resolution.tuplesForUpdate.push([local, server])
77
+ } else {
78
+ // server is older - can happen with clock skew - just mark as synced
79
+ this.resolution.recordsForSynced.push(local)
72
80
  }
73
81
  }
74
82
 
@@ -956,9 +956,15 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
956
956
  r._raw._changed = ''
957
957
  })
958
958
  })
959
+ const syncedBuilds = result.recordsForSynced.map((record) => {
960
+ return record.prepareUpdate((r) => {
961
+ r._raw._status = 'synced'
962
+ r._raw._changed = ''
963
+ })
964
+ })
959
965
 
960
966
  return [
961
- [...destroyedBuilds, ...createdBuilds, ...updatedBuilds, ...restoreDestroyBuilds],
967
+ [...destroyedBuilds, ...createdBuilds, ...updatedBuilds, ...restoreDestroyBuilds, ...syncedBuilds],
962
968
  [...restoreCreateBuilds],
963
969
  ]
964
970
  }
@@ -67,14 +67,18 @@ export class SyncTelemetry {
67
67
  Sentry,
68
68
  level,
69
69
  pretty,
70
- }: { Sentry: SentryLike; level?: keyof typeof SeverityLevel; pretty?: boolean }
70
+ }: { Sentry: SentryLike; level?: SeverityLevel | keyof typeof SeverityLevel; pretty?: boolean }
71
71
  ) {
72
72
  this.userScope = userScope
73
73
  this.Sentry = Sentry
74
- this.level =
75
- typeof level !== 'undefined' && level in SeverityLevel
76
- ? SeverityLevel[level]
77
- : SeverityLevel.LOG
74
+ const normalizedLevel =
75
+ typeof level === 'number'
76
+ ? level
77
+ : typeof level === 'string' && level in SeverityLevel
78
+ ? SeverityLevel[level]
79
+ : undefined
80
+
81
+ this.level = typeof normalizedLevel === 'number' ? normalizedLevel : SeverityLevel.LOG
78
82
  this.pretty = typeof pretty !== 'undefined' ? pretty : true
79
83
 
80
84
  watermelonLogger.log = (message: unknown) => this.log(message instanceof Error ? message : ['[Watermelon]', message].join(' '))
@@ -177,7 +181,6 @@ export class SyncTelemetry {
177
181
 
178
182
  _log(level: SeverityLevel, consoleMethod: 'info' | 'log' | 'warn' | 'error', message: unknown, extra?: any) {
179
183
  if (this.level > level || this.shouldIgnoreMessage(message)) return
180
-
181
184
  this._ignoreConsole = true
182
185
  console[consoleMethod](...this.formattedConsoleMessage(message, extra))
183
186
  this._ignoreConsole = false
@@ -131,6 +131,7 @@ export async function generateAuthSessionUrl(userId, redirectTo) {
131
131
  headers.Authorization = `Bearer ${globalConfig.sessionConfig.authToken}`
132
132
  }
133
133
 
134
+ // generate auth key
134
135
  const response = await fetch(`${baseUrl}/v1/auth-key`, {
135
136
  method: 'GET',
136
137
  headers,
@@ -144,11 +145,23 @@ export async function generateAuthSessionUrl(userId, redirectTo) {
144
145
  const authKeyResponse = await response.json()
145
146
  const authKey = authKeyResponse.data || authKeyResponse.auth_key
146
147
 
148
+ const absoluteRedirectTo = new URL(redirectTo)
149
+ const relativeRedirectTo = absoluteRedirectTo.pathname + absoluteRedirectTo.search
150
+
147
151
  const params = new URLSearchParams({
148
152
  user_id: userId.toString(),
149
153
  auth_key: authKey,
150
- redirect_to: redirectTo,
154
+ redirect_to: relativeRedirectTo,
151
155
  })
152
156
 
153
- return `${baseUrl}/v1/sessions/auth-key?${params.toString()}`
157
+ // generate link that will *consume* the auth key
158
+ if (globalConfig.isMA) {
159
+ if (!absoluteRedirectTo.hostname.endsWith('.musora.com')) {
160
+ throw new Error('Bad redirect URL - must be a musora.com domain')
161
+ }
162
+
163
+ return `${absoluteRedirectTo.origin}/auth?${params.toString()}`
164
+ } else {
165
+ throw new Error('Not implemented - MA deep links don\'t accept auth keys')
166
+ }
154
167
  }
@@ -513,6 +513,7 @@ export async function getRecentActivity({ page = 1, limit = 5, tabName = null }
513
513
  title: content.title,
514
514
  parent_id: content.parent_id || null,
515
515
  navigateTo: content.navigateTo,
516
+ artist_name: content.artist_name || null,
516
517
  }
517
518
  })
518
519
  return recentActivityData
@@ -792,12 +793,12 @@ async function formatPracticeMeta(practices = []) {
792
793
  return {
793
794
  id: practice.id,
794
795
  auto: practice.auto,
795
- thumbnail: practice.content_id ? content.thumbnail : practice.thumbnail_url || '',
796
- thumbnail_url: practice.content_id ? content.thumbnail : practice.thumbnail_url || '',
796
+ thumbnail: practice.content_id ? content?.thumbnail : practice.thumbnail_url || '',
797
+ thumbnail_url: practice.content_id ? content?.thumbnail : practice.thumbnail_url || '',
797
798
  duration: practice.duration_seconds || 0,
798
799
  duration_seconds: practice.duration_seconds || 0,
799
800
  content_url: content?.url || null,
800
- title: practice.content_id ? content.title : practice.title,
801
+ title: practice.content_id ? content?.title : practice?.title || practice.content_id,
801
802
  category_id: practice.category_id,
802
803
  instrument_id: practice.instrument_id,
803
804
  content_type: getFormattedType(content?.type || '', content?.brand || null),
@@ -808,6 +809,7 @@ async function formatPracticeMeta(practices = []) {
808
809
  content_slug: content?.slug || null,
809
810
  parent_id: content?.parent_id || null,
810
811
  navigateTo: content?.navigateTo || null,
812
+ artist_name: content?.artist_name || null,
811
813
  }
812
814
  })
813
815
  }
@@ -35,9 +35,10 @@ export function initializeSyncManager(userId) {
35
35
  },
36
36
  }
37
37
 
38
- SyncTelemetry.setInstance(new SyncTelemetry(userId, { Sentry: dummySentry, level: SeverityLevel.WARNING, pretty: false }))
38
+ const userScope = { initialId: userId, getCurrentId: () => userId }
39
+ SyncTelemetry.setInstance(new SyncTelemetry(userScope, { Sentry: dummySentry, level: SeverityLevel.WARNING, pretty: false }))
39
40
 
40
- const adapter = syncAdapter(userId)
41
+ const adapter = syncAdapter()
41
42
  const db = syncDatabaseFactory(adapter)
42
43
 
43
44
  const context = new SyncContext({
@@ -74,7 +75,7 @@ export function initializeSyncManager(userId) {
74
75
  stop: () => {},
75
76
  },
76
77
  })
77
- const manager = new SyncManager(context, db)
78
+ const manager = new SyncManager(userScope, context, db)
78
79
 
79
80
  const initialStrategy = manager.createStrategy(InitialStrategy)
80
81