musora-content-services 2.186.0 → 2.187.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,22 @@
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.187.0](https://github.com/railroadmedia/musora-content-services/compare/v2.186.1...v2.187.0) (2026-10-19)
6
+
7
+
8
+ ### Features
9
+
10
+ * **MU2-2024:** add disclaimer to the promotional campaign placement type ([9905117](https://github.com/railroadmedia/musora-content-services/commit/990511777622eca93758b5252c908900e484b6f5))
11
+
12
+ ### [2.186.1](https://github.com/railroadmedia/musora-content-services/compare/v2.186.0...v2.186.1) (2026-10-19)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * guard against null awards list when Sanity fetch fails ([#1053](https://github.com/railroadmedia/musora-content-services/issues/1053)) ([77f0bf7](https://github.com/railroadmedia/musora-content-services/commit/77f0bf711c2fcc5a5e47cb1a8d0328f670f5107d))
18
+ * preserve resume time when duplicating playlist progress ([#1045](https://github.com/railroadmedia/musora-content-services/issues/1045)) ([d62d394](https://github.com/railroadmedia/musora-content-services/commit/d62d394542b170741c818182405ca53efde046d1)), closes [#1079](https://github.com/railroadmedia/musora-content-services/issues/1079)
19
+ * replace @/ alias imports with relative paths in src ([bc22248](https://github.com/railroadmedia/musora-content-services/commit/bc22248264a029904c7f8bae403c257268da47c6))
20
+
5
21
  ## [2.186.0](https://github.com/railroadmedia/musora-content-services/compare/v2.185.1...v2.186.0) (2026-10-16)
6
22
 
7
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.186.0",
3
+ "version": "2.187.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -115,6 +115,7 @@ export type PromotionalCampaignDocument = {
115
115
  key: string
116
116
  title?: string
117
117
  body?: string
118
+ disclaimer?: string
118
119
  /** Resolved to a CDN url by the promotions query projection. */
119
120
  dark_mode_image?: string
120
121
  /** Resolved to a CDN url by the promotions query projection. */
@@ -132,6 +132,10 @@ class AwardDefinitionsService {
132
132
 
133
133
  const awards = await fetchSanity(query, true, { processNeedAccess: false, processPageType: false })
134
134
 
135
+ if (!awards) {
136
+ return
137
+ }
138
+
135
139
  this.definitions.clear()
136
140
  this.contentIndex.clear()
137
141
 
@@ -596,7 +596,25 @@ export async function contentStatusReset(contentId, collection = null, { skipPus
596
596
  }
597
597
 
598
598
  // does not have an offline variant because it's too deeply nested within the watch session flow.
599
- export async function saveContentProgress(
599
+ export async function saveContentProgress(contentId, collection, progress, currentSeconds, options = {}) {
600
+ collection = collection ?? { id: COLLECTION_ID_SELF, type: COLLECTION_TYPE.SELF }
601
+ const response = await saveProgressPercent(contentId, collection, progress, currentSeconds, options)
602
+ await saveResumeTime(contentId, collection, currentSeconds)
603
+ return response
604
+ }
605
+
606
+ // resume time is last-write-wins, so it bypasses the percent gate that drops regressed progress
607
+ async function saveResumeTime(contentId, collection, currentSeconds) {
608
+ if (currentSeconds === undefined || currentSeconds === null) return
609
+
610
+ const normalizedContentId = normalizeContentId(contentId)
611
+ await db.contentProgress.updateResumeTime(normalizedContentId, normalizeCollection(collection), currentSeconds, { skipPush: true })
612
+ if (collection.type === COLLECTION_TYPE.PLAYLIST) {
613
+ await db.contentProgress.updateResumeTime(normalizedContentId, null, currentSeconds, { skipPush: true })
614
+ }
615
+ }
616
+
617
+ async function saveProgressPercent(
600
618
  contentId,
601
619
  collection,
602
620
  progress,
@@ -608,7 +626,6 @@ export async function saveContentProgress(
608
626
  accessedDirectly = true,
609
627
  } = {},
610
628
  ) {
611
- collection = collection ?? { id: COLLECTION_ID_SELF, type: COLLECTION_TYPE.SELF }
612
629
  const isPlaylist = collection?.type === COLLECTION_TYPE.PLAYLIST
613
630
 
614
631
  let allProgresses = {}
@@ -5,8 +5,8 @@ import { Filters as f } from '../../lib/sanity/filter'
5
5
  import { groq } from '../../lib/sanity/groq'
6
6
  import type { GroqBuilder } from '../../lib/sanity/groq'
7
7
  import type { PromotionalCampaignDocument } from '../../lib/sanity/types/marketing'
8
- import { SanityQueryError } from '@/lib/sanity/runner'
9
- import { AsyncEither } from '@/lib/ads/async-either'
8
+ import { SanityQueryError } from '../../lib/sanity/runner'
9
+ import { AsyncEither } from '../../lib/ads/async-either'
10
10
 
11
11
  const excludeFromGeneratedIndex = [
12
12
  'campaignQuery',
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { HttpClient } from '../../infrastructure/http/HttpClient'
5
5
  import { globalConfig } from '../config.js'
6
- import { OnboardingRecommendedContent } from '@/services/user/onboarding'
6
+ import { OnboardingRecommendedContent } from '../user/onboarding'
7
7
 
8
8
 
9
9
  const baseUrl = `/api/my-path/v1/placement-quiz`
@@ -67,14 +67,40 @@ export const save = async (
67
67
  progress: number,
68
68
  collection?: CollectionParameter,
69
69
  currentSeconds?: number,
70
+ options: SaveOptions = {}
71
+ ) => {
72
+ const activeCollection = withDefaultCollection(collection)
73
+ const response = await saveProgressPercent(contentId, progress, activeCollection, currentSeconds, options)
74
+ await saveResumeTime(contentId, activeCollection, currentSeconds)
75
+ return response
76
+ }
77
+
78
+ // resume time is last-write-wins, so it bypasses the percent gate that drops regressed progress
79
+ const saveResumeTime = async (
80
+ contentId: number,
81
+ collection: CollectionParameter,
82
+ currentSeconds: number | undefined
83
+ ): Promise<void> => {
84
+ if (currentSeconds === undefined || currentSeconds === null) return
85
+
86
+ await db.contentProgress.updateResumeTime(contentId, collection, currentSeconds, { skipPush: true })
87
+ if (isPlaylistCollection(collection)) {
88
+ await db.contentProgress.updateResumeTime(contentId, null, currentSeconds, { skipPush: true })
89
+ }
90
+ }
91
+
92
+ const saveProgressPercent = async (
93
+ contentId: number,
94
+ progress: number,
95
+ activeCollection: CollectionParameter,
96
+ currentSeconds: number | undefined,
70
97
  {
71
98
  isOffline = false,
72
99
  hierarchy = null,
73
100
  skipPush = false,
74
101
  accessedDirectly = true,
75
- }: SaveOptions = {}
102
+ }: SaveOptions
76
103
  ) => {
77
- const activeCollection = withDefaultCollection(collection)
78
104
  const isPlaylist = isPlaylistCollection(activeCollection)
79
105
 
80
106
  let progresses: Record<number, number> = { [contentId]: progress }
@@ -12,6 +12,8 @@ interface MetadataParameter {
12
12
  parent_id: number
13
13
  }
14
14
 
15
+ const MIN_INITIAL_RESUME_TIME_SECONDS = 10
16
+
15
17
  export default class ProgressRepository extends SyncRepository<ContentProgress> {
16
18
 
17
19
  static collectionTypeFilter(
@@ -185,11 +187,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
185
187
  r.content_type = metadata.type
186
188
  r.content_parent_id = metadata.parent_id
187
189
 
188
- if (typeof resumeTime != 'undefined') {
189
- if (resumeTime >= 10 || r.resume_time_seconds !== null) {
190
- r.resume_time_seconds = Math.floor(resumeTime)
191
- }
192
- }
190
+ ProgressRepository.applyResumeTime(r, resumeTime)
193
191
 
194
192
  if (accessedDirectly && r.collection_type === COLLECTION_TYPE.SELF) {
195
193
  r.last_interacted_a_la_carte = r.updated_at
@@ -263,6 +261,29 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
263
261
  //todo add event emitting for bulk updates?
264
262
  }
265
263
 
264
+ async updateResumeTime(contentId: number, collection: CollectionParameter | null, resumeTime: number, { skipPush = false } = {}) {
265
+ const id = ContentProgress.generateId(contentId, collection)
266
+ const existing = await this.readOne(id)
267
+ if (!existing.data) return
268
+
269
+ const currentResumeTime = existing.data.resume_time_seconds
270
+ const isUnchanged = currentResumeTime === Math.floor(resumeTime)
271
+ if (isUnchanged || !ProgressRepository.shouldWriteResumeTime(currentResumeTime, resumeTime)) return
272
+
273
+ return this.upsertOne(id, (r) => ProgressRepository.applyResumeTime(r, resumeTime), { skipPush })
274
+ }
275
+
276
+ private static shouldWriteResumeTime(currentResumeTime: number | null, resumeTime: number) {
277
+ return resumeTime >= MIN_INITIAL_RESUME_TIME_SECONDS || currentResumeTime !== null
278
+ }
279
+
280
+ private static applyResumeTime(r: ContentProgress, resumeTime: number | undefined) {
281
+ if (typeof resumeTime == 'undefined') return
282
+ if (ProgressRepository.shouldWriteResumeTime(r.resume_time_seconds, resumeTime)) {
283
+ r.resume_time_seconds = Math.floor(resumeTime)
284
+ }
285
+ }
286
+
266
287
  eraseProgress(contentId: number, collection: CollectionParameter | null, { skipPush = false } = {}) {
267
288
  return this.deleteOne(ContentProgress.generateId(contentId, collection), { skipPush })
268
289
  }
@@ -1 +1 @@
1
- export const MCS_VERSION = '2.186.0'
1
+ export const MCS_VERSION = '2.187.0'