@growth-labs/cms 0.8.2 → 0.8.4
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/dist/engine/ai-dek-notifications.d.ts +2 -0
- package/dist/engine/ai-dek-notifications.d.ts.map +1 -1
- package/dist/engine/ai-dek-notifications.js +11 -2
- package/dist/engine/ai-dek-notifications.js.map +1 -1
- package/dist/engine/ai-dek.d.ts +23 -0
- package/dist/engine/ai-dek.d.ts.map +1 -1
- package/dist/engine/ai-dek.js +94 -31
- package/dist/engine/ai-dek.js.map +1 -1
- package/dist/engine/foundry-dispatch.d.ts +13 -3
- package/dist/engine/foundry-dispatch.d.ts.map +1 -1
- package/dist/engine/foundry-dispatch.js +115 -39
- package/dist/engine/foundry-dispatch.js.map +1 -1
- package/dist/engine/fronts-publish-intent.d.ts +55 -0
- package/dist/engine/fronts-publish-intent.d.ts.map +1 -1
- package/dist/engine/fronts-publish-intent.js +187 -57
- package/dist/engine/fronts-publish-intent.js.map +1 -1
- package/dist/engine/fronts-publish.d.ts +11 -0
- package/dist/engine/fronts-publish.d.ts.map +1 -1
- package/dist/engine/fronts-publish.js +135 -10
- package/dist/engine/fronts-publish.js.map +1 -1
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +2 -2
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/podcast-source.d.ts +3 -2
- package/dist/engine/podcast-source.d.ts.map +1 -1
- package/dist/engine/podcast-source.js +6 -4
- package/dist/engine/podcast-source.js.map +1 -1
- package/dist/engine/publication-current-guard.d.ts +7 -0
- package/dist/engine/publication-current-guard.d.ts.map +1 -1
- package/dist/engine/publication-current-guard.js +66 -0
- package/dist/engine/publication-current-guard.js.map +1 -1
- package/dist/engine/publication.d.ts +30 -0
- package/dist/engine/publication.d.ts.map +1 -1
- package/dist/engine/publication.js +306 -34
- package/dist/engine/publication.js.map +1 -1
- package/dist/engine/publisher.d.ts.map +1 -1
- package/dist/engine/publisher.js +32 -19
- package/dist/engine/publisher.js.map +1 -1
- package/dist/engine/soft-delete.d.ts +3 -1
- package/dist/engine/soft-delete.d.ts.map +1 -1
- package/dist/engine/soft-delete.js +21 -5
- package/dist/engine/soft-delete.js.map +1 -1
- package/dist/engine/video-source.d.ts +4 -2
- package/dist/engine/video-source.d.ts.map +1 -1
- package/dist/engine/video-source.js +7 -4
- package/dist/engine/video-source.js.map +1 -1
- package/dist/routes/content.d.ts +14 -0
- package/dist/routes/content.d.ts.map +1 -1
- package/dist/routes/content.js +56 -28
- package/dist/routes/content.js.map +1 -1
- package/dist/routes/context.d.ts +6 -0
- package/dist/routes/context.d.ts.map +1 -1
- package/dist/routes/context.js.map +1 -1
- package/dist/routes/fronts-publish.d.ts +2 -0
- package/dist/routes/fronts-publish.d.ts.map +1 -1
- package/dist/routes/fronts-publish.js +178 -2
- package/dist/routes/fronts-publish.js.map +1 -1
- package/dist/schema/insights-ingest.d.ts +42 -42
- package/dist/schema/migrations.d.ts.map +1 -1
- package/dist/schema/migrations.js +8 -0
- package/dist/schema/migrations.js.map +1 -1
- package/dist/schema/types.d.ts +6 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js.map +1 -1
- package/dist/surveys/schema.d.ts +48 -48
- package/dist/ui/api/_content-config.d.ts.map +1 -1
- package/dist/ui/api/_content-config.js +7 -0
- package/dist/ui/api/_content-config.js.map +1 -1
- package/dist/ui/api/fronts/schedule.d.ts +2 -0
- package/dist/ui/api/fronts/schedule.d.ts.map +1 -1
- package/dist/ui/api/fronts/schedule.js +2 -0
- package/dist/ui/api/fronts/schedule.js.map +1 -1
- package/dist/ui/editor/ContentForm.d.ts.map +1 -1
- package/dist/ui/editor/ContentForm.js +2 -2
- package/dist/ui/editor/ContentForm.js.map +1 -1
- package/migrations/0030_media_publication_guard.sql +5 -0
- package/package.json +1 -1
- package/src/engine/ai-dek-notifications.ts +19 -2
- package/src/engine/ai-dek.ts +131 -40
- package/src/engine/foundry-dispatch.ts +189 -49
- package/src/engine/fronts-publish-intent.ts +295 -67
- package/src/engine/fronts-publish.ts +167 -11
- package/src/engine/index.ts +4 -0
- package/src/engine/podcast-source.ts +6 -4
- package/src/engine/publication-current-guard.ts +86 -0
- package/src/engine/publication.ts +411 -59
- package/src/engine/publisher.ts +37 -17
- package/src/engine/soft-delete.ts +28 -8
- package/src/engine/video-source.ts +7 -4
- package/src/routes/content.ts +78 -35
- package/src/routes/context.ts +6 -0
- package/src/routes/fronts-publish.ts +213 -2
- package/src/schema/migrations.ts +8 -0
- package/src/schema/types.ts +6 -0
- package/src/ui/api/_content-config.ts +15 -0
- package/src/ui/api/fronts/schedule.ts +4 -0
- package/src/ui/editor/ContentForm.tsx +4 -5
|
@@ -304,15 +304,53 @@ export function parseFrontsChannelTargets(
|
|
|
304
304
|
return normalizeChannelTargets(parsed)
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
/** Host defaults are opt-in; an invalid configured list must fail before a save. */
|
|
308
|
+
export function parseFrontsDefaultChannelTargets(
|
|
309
|
+
value: unknown,
|
|
310
|
+
):
|
|
311
|
+
| { targets: string[] }
|
|
312
|
+
| { error: 'missing_fronts_default_channel_targets' | 'invalid_fronts_default_channel_targets' } {
|
|
313
|
+
if (value === undefined) return { error: 'missing_fronts_default_channel_targets' }
|
|
314
|
+
if (!Array.isArray(value)) return { error: 'invalid_fronts_default_channel_targets' }
|
|
315
|
+
const parsed = normalizeChannelTargets(value)
|
|
316
|
+
return 'code' in parsed ? { error: 'invalid_fronts_default_channel_targets' } : parsed
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
interface ScheduledTargetsSnapshot {
|
|
320
|
+
id: string
|
|
321
|
+
canonical_version: number | null
|
|
322
|
+
publish_at: number
|
|
323
|
+
updated_at: number | null
|
|
324
|
+
fronts_publish_trigger_token: string | null
|
|
325
|
+
fronts_publish_status: string | null
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
interface ChannelTargetsWriteOptions {
|
|
329
|
+
onlyIfNull?: boolean
|
|
330
|
+
expectedScheduled?: ScheduledTargetsSnapshot
|
|
331
|
+
}
|
|
332
|
+
|
|
307
333
|
/**
|
|
308
334
|
* Arm an item's publish targets. Rejects an empty or non-string list loudly
|
|
309
335
|
* instead of storing something that would later hydrate as "complete".
|
|
310
336
|
*/
|
|
337
|
+
export function setFrontsChannelTargets(
|
|
338
|
+
db: D1Database,
|
|
339
|
+
contentId: string,
|
|
340
|
+
targets: readonly string[],
|
|
341
|
+
): Promise<void>
|
|
342
|
+
export function setFrontsChannelTargets(
|
|
343
|
+
db: D1Database,
|
|
344
|
+
contentId: string,
|
|
345
|
+
targets: readonly string[],
|
|
346
|
+
options: ChannelTargetsWriteOptions,
|
|
347
|
+
): Promise<boolean>
|
|
311
348
|
export async function setFrontsChannelTargets(
|
|
312
349
|
db: D1Database,
|
|
313
350
|
contentId: string,
|
|
314
351
|
targets: readonly string[],
|
|
315
|
-
|
|
352
|
+
options?: ChannelTargetsWriteOptions,
|
|
353
|
+
): Promise<void | boolean> {
|
|
316
354
|
const normalized = normalizeChannelTargets(targets)
|
|
317
355
|
if ('code' in normalized) {
|
|
318
356
|
throw new FrontsPublishIntentError(
|
|
@@ -321,10 +359,83 @@ export async function setFrontsChannelTargets(
|
|
|
321
359
|
'targets must be a non-empty list of non-empty strings',
|
|
322
360
|
)
|
|
323
361
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
362
|
+
const predicates = ['id = ?']
|
|
363
|
+
const values: unknown[] = [JSON.stringify(normalized.targets), contentId]
|
|
364
|
+
if (options?.onlyIfNull || options?.expectedScheduled) {
|
|
365
|
+
predicates.push('fronts_channel_targets IS NULL', 'deleted_at IS NULL')
|
|
366
|
+
}
|
|
367
|
+
if (options?.expectedScheduled) {
|
|
368
|
+
const expected = options.expectedScheduled
|
|
369
|
+
if (expected.id !== contentId)
|
|
370
|
+
throw new Error('Channel target snapshot belongs to different content')
|
|
371
|
+
predicates.push(
|
|
372
|
+
"status = 'scheduled'",
|
|
373
|
+
'canonical_version IS ?',
|
|
374
|
+
'publish_at IS ?',
|
|
375
|
+
'updated_at IS ?',
|
|
376
|
+
'fronts_publish_trigger_token IS ?',
|
|
377
|
+
'fronts_publish_status IS ?',
|
|
378
|
+
)
|
|
379
|
+
values.push(
|
|
380
|
+
expected.canonical_version,
|
|
381
|
+
expected.publish_at,
|
|
382
|
+
expected.updated_at,
|
|
383
|
+
expected.fronts_publish_trigger_token,
|
|
384
|
+
expected.fronts_publish_status,
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
const result = await db
|
|
388
|
+
.prepare(
|
|
389
|
+
`UPDATE content_items SET fronts_channel_targets = ? WHERE ${predicates.join(' AND ')}`,
|
|
390
|
+
)
|
|
391
|
+
.bind(...values)
|
|
327
392
|
.run()
|
|
393
|
+
// Preserve the original explicit setter contract. CAS callers need a proved
|
|
394
|
+
// change count, including an honest false when another writer won.
|
|
395
|
+
if (!options) return
|
|
396
|
+
const changes = result.meta?.changes
|
|
397
|
+
if (
|
|
398
|
+
result.success === false ||
|
|
399
|
+
typeof changes !== 'number' ||
|
|
400
|
+
!Number.isInteger(changes) ||
|
|
401
|
+
changes < 0 ||
|
|
402
|
+
changes > 1
|
|
403
|
+
) {
|
|
404
|
+
throw new Error('Fronts channel target write did not report a valid change count')
|
|
405
|
+
}
|
|
406
|
+
return changes === 1
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** A bounded repair requested by Go; all rows keep their current publish/media identity. */
|
|
410
|
+
export async function backfillFrontsChannelTargets(
|
|
411
|
+
db: D1Database,
|
|
412
|
+
targets: readonly string[],
|
|
413
|
+
limit: number,
|
|
414
|
+
): Promise<{ examined: number; armed: number; skipped: number; contentIds: string[] }> {
|
|
415
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50)
|
|
416
|
+
throw new Error('Invalid channel target backfill limit')
|
|
417
|
+
const selected = await db
|
|
418
|
+
.prepare(`SELECT id, canonical_version, publish_at, updated_at,
|
|
419
|
+
fronts_publish_trigger_token, fronts_publish_status
|
|
420
|
+
FROM content_items
|
|
421
|
+
WHERE status = 'scheduled' AND deleted_at IS NULL
|
|
422
|
+
AND publish_at IS NOT NULL AND fronts_channel_targets IS NULL
|
|
423
|
+
ORDER BY publish_at ASC, id ASC LIMIT ?`)
|
|
424
|
+
.bind(limit)
|
|
425
|
+
.all<ScheduledTargetsSnapshot>()
|
|
426
|
+
if (selected.success === false || !Array.isArray(selected.results))
|
|
427
|
+
throw new Error('Fronts channel target selection failed')
|
|
428
|
+
const contentIds: string[] = []
|
|
429
|
+
for (const row of selected.results) {
|
|
430
|
+
if (await setFrontsChannelTargets(db, row.id, targets, { expectedScheduled: row }))
|
|
431
|
+
contentIds.push(row.id)
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
examined: selected.results.length,
|
|
435
|
+
armed: contentIds.length,
|
|
436
|
+
skipped: selected.results.length - contentIds.length,
|
|
437
|
+
contentIds,
|
|
438
|
+
}
|
|
328
439
|
}
|
|
329
440
|
|
|
330
441
|
// --- hydration -------------------------------------------------------------
|
|
@@ -427,6 +538,7 @@ interface VideoRefRow {
|
|
|
427
538
|
processing_source_url: string | null
|
|
428
539
|
processing_source_kind: string | null
|
|
429
540
|
processing_trigger_token: string | null
|
|
541
|
+
processing_state: string | null
|
|
430
542
|
processing_correlation_id: string | null
|
|
431
543
|
}
|
|
432
544
|
|
|
@@ -438,6 +550,7 @@ interface PodcastRefRow {
|
|
|
438
550
|
processing_source_url: string | null
|
|
439
551
|
processing_source_kind: string | null
|
|
440
552
|
processing_trigger_token: string | null
|
|
553
|
+
processing_state: string | null
|
|
441
554
|
}
|
|
442
555
|
|
|
443
556
|
interface MediaRow {
|
|
@@ -571,7 +684,7 @@ async function loadContentRefSources(
|
|
|
571
684
|
.prepare(
|
|
572
685
|
`SELECT content_id, hls_ready, hls_manifest_url, hls_poster_url, duration_seconds,
|
|
573
686
|
thumbnail_image_id, processing_source_url, processing_source_kind,
|
|
574
|
-
processing_trigger_token, processing_correlation_id
|
|
687
|
+
processing_trigger_token, processing_correlation_id, processing_state
|
|
575
688
|
FROM video_content WHERE content_id IN (${placeholders(videoIds.length)})`,
|
|
576
689
|
)
|
|
577
690
|
.bind(...videoIds)
|
|
@@ -583,7 +696,7 @@ async function loadContentRefSources(
|
|
|
583
696
|
if (podcastIds.length > 0) {
|
|
584
697
|
const res = await db
|
|
585
698
|
.prepare(
|
|
586
|
-
`SELECT content_id, transcript, audio_r2_key, duration_seconds, processing_source_url, processing_source_kind, processing_trigger_token FROM podcast_content
|
|
699
|
+
`SELECT content_id, transcript, audio_r2_key, duration_seconds, processing_source_url, processing_source_kind, processing_trigger_token, processing_state FROM podcast_content
|
|
587
700
|
WHERE content_id IN (${placeholders(podcastIds.length)})`,
|
|
588
701
|
)
|
|
589
702
|
.bind(...podcastIds)
|
|
@@ -617,6 +730,178 @@ async function loadContentRefSources(
|
|
|
617
730
|
return { articles, videos, podcasts, media }
|
|
618
731
|
}
|
|
619
732
|
|
|
733
|
+
/** Preparation and publication projections share the same source validation. */
|
|
734
|
+
async function videoSourceRef(
|
|
735
|
+
row: IntentCandidateRow,
|
|
736
|
+
found: VideoRefRow | undefined,
|
|
737
|
+
): Promise<FrontsPublishVideoSourceRef | null> {
|
|
738
|
+
const correlation = found?.processing_correlation_id?.trim()
|
|
739
|
+
if (
|
|
740
|
+
!found ||
|
|
741
|
+
!validPodcastSourceUrl(found.processing_source_url) ||
|
|
742
|
+
!videoSourceIngestableOverHttps(found.processing_source_kind) ||
|
|
743
|
+
!correlation ||
|
|
744
|
+
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(correlation)
|
|
745
|
+
)
|
|
746
|
+
return null
|
|
747
|
+
if (
|
|
748
|
+
typeof found.duration_seconds !== 'number' ||
|
|
749
|
+
!Number.isFinite(found.duration_seconds) ||
|
|
750
|
+
found.duration_seconds <= 0
|
|
751
|
+
) {
|
|
752
|
+
throw new FrontsPublishIntentError(
|
|
753
|
+
'video_source_duration_unavailable',
|
|
754
|
+
row.id,
|
|
755
|
+
'Video source duration is not verified.',
|
|
756
|
+
)
|
|
757
|
+
}
|
|
758
|
+
if (found.processing_state !== 'queued' || !found.processing_trigger_token?.trim()) return null
|
|
759
|
+
return {
|
|
760
|
+
kind: 'https',
|
|
761
|
+
url: found.processing_source_url,
|
|
762
|
+
duration_seconds: found.duration_seconds,
|
|
763
|
+
generation: await frontsVideoSourceGeneration({
|
|
764
|
+
content_id: row.id,
|
|
765
|
+
slug: row.slug,
|
|
766
|
+
canonical_version: row.canonical_version as number,
|
|
767
|
+
processing_source_url: found.processing_source_url,
|
|
768
|
+
processing_source_kind: found.processing_source_kind,
|
|
769
|
+
processing_trigger_token: found.processing_trigger_token,
|
|
770
|
+
}),
|
|
771
|
+
correlation_id: correlation,
|
|
772
|
+
visibility: row.visibility === 'premium' ? 'premium' : 'free',
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async function podcastSourceRef(
|
|
777
|
+
row: IntentCandidateRow,
|
|
778
|
+
found: PodcastRefRow | undefined,
|
|
779
|
+
): Promise<FrontsPublishPodcastSourceRef | null> {
|
|
780
|
+
if (
|
|
781
|
+
!found ||
|
|
782
|
+
!validPodcastSourceUrl(found.processing_source_url) ||
|
|
783
|
+
found.processing_source_kind !== 'https'
|
|
784
|
+
)
|
|
785
|
+
return null
|
|
786
|
+
if (
|
|
787
|
+
typeof found.duration_seconds !== 'number' ||
|
|
788
|
+
!Number.isFinite(found.duration_seconds) ||
|
|
789
|
+
found.duration_seconds <= 0
|
|
790
|
+
) {
|
|
791
|
+
throw new FrontsPublishIntentError(
|
|
792
|
+
'podcast_source_duration_unavailable',
|
|
793
|
+
row.id,
|
|
794
|
+
'Podcast source duration is not verified.',
|
|
795
|
+
)
|
|
796
|
+
}
|
|
797
|
+
if (found.processing_state !== 'queued' || !found.processing_trigger_token?.trim()) return null
|
|
798
|
+
return {
|
|
799
|
+
kind: 'https',
|
|
800
|
+
url: found.processing_source_url,
|
|
801
|
+
duration_seconds: found.duration_seconds,
|
|
802
|
+
generation: await frontsPodcastSourceGeneration({
|
|
803
|
+
content_id: row.id,
|
|
804
|
+
slug: row.slug,
|
|
805
|
+
canonical_version: row.canonical_version as number,
|
|
806
|
+
processing_source_url: found.processing_source_url,
|
|
807
|
+
processing_source_kind: found.processing_source_kind,
|
|
808
|
+
processing_trigger_token: found.processing_trigger_token,
|
|
809
|
+
}),
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/** A captured source is preparation authority, never publication authority. */
|
|
814
|
+
export interface FrontsMediaPreparation {
|
|
815
|
+
content_id: string
|
|
816
|
+
type: 'video' | 'podcast'
|
|
817
|
+
slug: string
|
|
818
|
+
channel: string | null
|
|
819
|
+
canonical_version: number
|
|
820
|
+
/** The actual slot, if any; never invented for a draft. */
|
|
821
|
+
publish_at: number | null
|
|
822
|
+
/** Actual capture time; unknown for a legacy token. */
|
|
823
|
+
captured_at: number | null
|
|
824
|
+
video_source: FrontsPublishVideoSourceRef | null
|
|
825
|
+
podcast_source: FrontsPublishPodcastSourceRef | null
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
export interface FrontsMediaPreparations {
|
|
829
|
+
version: 1
|
|
830
|
+
items: FrontsMediaPreparation[]
|
|
831
|
+
incomplete: Array<{ content_id: string; code: string; message: string }>
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Extend the existing authenticated poll with captured media, independently of
|
|
836
|
+
* publish_at/status=scheduled. No publish token, fake deadline or new queue is
|
|
837
|
+
* minted by this read. A finite complete response fails loudly on overflow.
|
|
838
|
+
*/
|
|
839
|
+
export async function hydrateFrontsMediaPreparations(
|
|
840
|
+
db: D1Database,
|
|
841
|
+
options: Pick<
|
|
842
|
+
HydrateFrontsPublishIntentsOptions,
|
|
843
|
+
'limit' | 'videoSourceProductionEnabled' | 'podcastSourceProductionEnabled'
|
|
844
|
+
> = {},
|
|
845
|
+
): Promise<FrontsMediaPreparations> {
|
|
846
|
+
const limit = Math.min(Math.max(1, Math.floor(options.limit ?? 200)), 1000)
|
|
847
|
+
const result: FrontsMediaPreparations = { version: 1, items: [], incomplete: [] }
|
|
848
|
+
const candidates = await db
|
|
849
|
+
.prepare(`SELECT ${CANDIDATE_COLUMNS} FROM content_items
|
|
850
|
+
WHERE deleted_at IS NULL AND status IN ('draft', 'review', 'scheduled', 'published')
|
|
851
|
+
AND ((type = 'video' AND ? = 1 AND EXISTS (
|
|
852
|
+
SELECT 1 FROM video_content v WHERE v.content_id = content_items.id
|
|
853
|
+
AND v.processing_trigger_token IS NOT NULL AND v.processing_state = 'queued'
|
|
854
|
+
AND (v.hls_ready <> 1 OR v.hls_manifest_url IS NULL OR v.transcript_ready <> 1)))
|
|
855
|
+
OR (type = 'podcast' AND ? = 1 AND EXISTS (
|
|
856
|
+
SELECT 1 FROM podcast_content p WHERE p.content_id = content_items.id
|
|
857
|
+
AND p.processing_trigger_token IS NOT NULL AND p.processing_state = 'queued'
|
|
858
|
+
AND (trim(COALESCE(p.audio_r2_key, '')) = '' OR trim(COALESCE(p.transcript, '')) = ''))))
|
|
859
|
+
ORDER BY created_at ASC, id ASC LIMIT ?`)
|
|
860
|
+
.bind(
|
|
861
|
+
options.videoSourceProductionEnabled === false ? 0 : 1,
|
|
862
|
+
options.podcastSourceProductionEnabled === false ? 0 : 1,
|
|
863
|
+
limit + 1,
|
|
864
|
+
)
|
|
865
|
+
.all<IntentCandidateRow>()
|
|
866
|
+
const rows = candidates.results ?? []
|
|
867
|
+
if (rows.length > limit) throw new FrontsScheduleOverflowError(limit)
|
|
868
|
+
const refs = await loadContentRefSources(db, rows)
|
|
869
|
+
for (const row of rows) {
|
|
870
|
+
try {
|
|
871
|
+
if (!Number.isInteger(row.canonical_version) || (row.canonical_version ?? 0) <= 0) {
|
|
872
|
+
throw new FrontsPublishIntentError('invalid_canonical_version', row.id)
|
|
873
|
+
}
|
|
874
|
+
const video = row.type === 'video' ? await videoSourceRef(row, refs.videos.get(row.id)) : null
|
|
875
|
+
const podcast =
|
|
876
|
+
row.type === 'podcast' ? await podcastSourceRef(row, refs.podcasts.get(row.id)) : null
|
|
877
|
+
if (!video && !podcast)
|
|
878
|
+
throw new FrontsPublishIntentError(
|
|
879
|
+
row.type === 'video' ? 'video_media_not_ready' : 'missing_podcast_audio',
|
|
880
|
+
row.id,
|
|
881
|
+
'Captured source is incomplete or not ingestable.',
|
|
882
|
+
)
|
|
883
|
+
const token = (row.type === 'video' ? refs.videos.get(row.id) : refs.podcasts.get(row.id))
|
|
884
|
+
?.processing_trigger_token
|
|
885
|
+
const capturedAt = /^fronts-source-v2:([1-9][0-9]*):[0-9a-f-]{36}$/.exec(token ?? '')?.[1]
|
|
886
|
+
result.items.push({
|
|
887
|
+
content_id: row.id,
|
|
888
|
+
type: row.type as 'video' | 'podcast',
|
|
889
|
+
slug: row.slug,
|
|
890
|
+
channel: row.channel,
|
|
891
|
+
canonical_version: row.canonical_version as number,
|
|
892
|
+
publish_at: row.publish_at,
|
|
893
|
+
captured_at: capturedAt ? Number(capturedAt) : null,
|
|
894
|
+
video_source: video,
|
|
895
|
+
podcast_source: podcast,
|
|
896
|
+
})
|
|
897
|
+
} catch (error) {
|
|
898
|
+
if (!(error instanceof FrontsPublishIntentError)) throw error
|
|
899
|
+
result.incomplete.push({ content_id: row.id, code: error.code, message: error.message })
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return result
|
|
903
|
+
}
|
|
904
|
+
|
|
620
905
|
/** Build one intent, throwing FrontsPublishIntentError on any missing required value. */
|
|
621
906
|
async function buildIntent(
|
|
622
907
|
row: IntentCandidateRow,
|
|
@@ -739,41 +1024,8 @@ async function buildContentRefs(
|
|
|
739
1024
|
message:
|
|
740
1025
|
'a video intent requires video_content.hls_ready = 1 and a non-empty hls_manifest_url',
|
|
741
1026
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
if (
|
|
745
|
-
found &&
|
|
746
|
-
validPodcastSourceUrl(sourceUrl) &&
|
|
747
|
-
videoSourceIngestableOverHttps(found.processing_source_kind) &&
|
|
748
|
-
correlationId &&
|
|
749
|
-
/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(correlationId)
|
|
750
|
-
) {
|
|
751
|
-
if (
|
|
752
|
-
typeof found.duration_seconds !== 'number' ||
|
|
753
|
-
!Number.isFinite(found.duration_seconds) ||
|
|
754
|
-
found.duration_seconds <= 0
|
|
755
|
-
) {
|
|
756
|
-
throw new FrontsPublishIntentError(
|
|
757
|
-
'video_source_duration_unavailable',
|
|
758
|
-
row.id,
|
|
759
|
-
'Video source duration is not verified.',
|
|
760
|
-
)
|
|
761
|
-
}
|
|
762
|
-
videoSource = {
|
|
763
|
-
kind: 'https',
|
|
764
|
-
url: sourceUrl,
|
|
765
|
-
duration_seconds: found.duration_seconds,
|
|
766
|
-
generation: await frontsVideoSourceGeneration({
|
|
767
|
-
content_id: row.id,
|
|
768
|
-
slug: row.slug,
|
|
769
|
-
canonical_version: row.canonical_version as number,
|
|
770
|
-
processing_source_url: found.processing_source_url,
|
|
771
|
-
processing_source_kind: found.processing_source_kind,
|
|
772
|
-
processing_trigger_token: found.processing_trigger_token,
|
|
773
|
-
}),
|
|
774
|
-
correlation_id: correlationId,
|
|
775
|
-
visibility: row.visibility === 'premium' ? 'premium' : 'free',
|
|
776
|
-
}
|
|
1027
|
+
videoSource = await videoSourceRef(row, found)
|
|
1028
|
+
if (videoSource) {
|
|
777
1029
|
incomplete = {
|
|
778
1030
|
content_id: row.id,
|
|
779
1031
|
code: failure.code,
|
|
@@ -824,32 +1076,8 @@ async function buildContentRefs(
|
|
|
824
1076
|
code: 'missing_podcast_audio' as const,
|
|
825
1077
|
message: 'Podcast audio is not ready.',
|
|
826
1078
|
}
|
|
827
|
-
|
|
828
|
-
if (
|
|
829
|
-
if (
|
|
830
|
-
typeof found.duration_seconds !== 'number' ||
|
|
831
|
-
!Number.isFinite(found.duration_seconds) ||
|
|
832
|
-
found.duration_seconds <= 0
|
|
833
|
-
) {
|
|
834
|
-
throw new FrontsPublishIntentError(
|
|
835
|
-
'podcast_source_duration_unavailable',
|
|
836
|
-
row.id,
|
|
837
|
-
'Podcast source duration is not verified.',
|
|
838
|
-
)
|
|
839
|
-
}
|
|
840
|
-
podcastSource = {
|
|
841
|
-
kind: 'https',
|
|
842
|
-
url: sourceUrl,
|
|
843
|
-
duration_seconds: found.duration_seconds,
|
|
844
|
-
generation: await frontsPodcastSourceGeneration({
|
|
845
|
-
content_id: row.id,
|
|
846
|
-
slug: row.slug,
|
|
847
|
-
canonical_version: row.canonical_version as number,
|
|
848
|
-
processing_source_url: found.processing_source_url,
|
|
849
|
-
processing_source_kind: found.processing_source_kind,
|
|
850
|
-
processing_trigger_token: found.processing_trigger_token,
|
|
851
|
-
}),
|
|
852
|
-
}
|
|
1079
|
+
podcastSource = await podcastSourceRef(row, found)
|
|
1080
|
+
if (podcastSource) {
|
|
853
1081
|
incomplete = {
|
|
854
1082
|
content_id: row.id,
|
|
855
1083
|
code: failure.code,
|