98 lines
2.9 KiB
JavaScript
98 lines
2.9 KiB
JavaScript
/**
|
|
* C02-C15 remote full-page audio manifest v1.
|
|
*
|
|
* Import contract for a future reviewed batch:
|
|
*
|
|
* 'S01-C09-P05': Object.freeze({
|
|
* pageId: 'S01-C09-P05',
|
|
* chapterNumber: 9,
|
|
* pageNumber: 5,
|
|
* assetId: 'audio.page.s01.c09.p05',
|
|
* durationSeconds: 42.1,
|
|
* reviewStatus: 'approved',
|
|
* })
|
|
*
|
|
* The matching releaseAssetManifest entry must independently be an approved,
|
|
* remote-only, immutable hash-named audio asset. Automated QA, pending human
|
|
* listening, and missing CDN records must never be relabelled as approved.
|
|
*/
|
|
|
|
const REMOTE_PAGE_AUDIO_MANIFEST_VERSION = 1
|
|
const REMOTE_PAGE_AUDIO_ID_PATTERN = /^S01-C(0[2-9]|1[0-5])-P(0[1-8])$/
|
|
const SHA256_PATTERN = /^[a-f0-9]{64}$/
|
|
|
|
// Deliberately empty until full-page tracks pass the required human reviews
|
|
// and their immutable CDN objects are available. This is a release gate, not
|
|
// a placeholder that the UI may treat as playable.
|
|
const remotePageAudioPages = Object.freeze({})
|
|
|
|
function clean(value) {
|
|
return typeof value === 'string' ? value.trim() : ''
|
|
}
|
|
|
|
function remotePageAudioAssetId(pageId) {
|
|
const stablePageId = clean(pageId)
|
|
const match = REMOTE_PAGE_AUDIO_ID_PATTERN.exec(stablePageId)
|
|
if (!match) return ''
|
|
return `audio.page.s01.c${match[1]}.p${match[2]}`
|
|
}
|
|
|
|
function getApprovedRemotePageAudio(
|
|
pageId,
|
|
releaseAssets = {},
|
|
pageManifest = remotePageAudioPages,
|
|
) {
|
|
const stablePageId = clean(pageId)
|
|
const match = REMOTE_PAGE_AUDIO_ID_PATTERN.exec(stablePageId)
|
|
if (!match) return null
|
|
|
|
const entry = pageManifest && pageManifest[stablePageId]
|
|
if (!entry || typeof entry !== 'object') return null
|
|
const chapterNumber = Number(match[1])
|
|
const pageNumber = Number(match[2])
|
|
const assetId = remotePageAudioAssetId(stablePageId)
|
|
const durationSeconds = Number(entry.durationSeconds)
|
|
if (
|
|
clean(entry.reviewStatus).toLowerCase() !== 'approved'
|
|
|| entry.pageId !== stablePageId
|
|
|| Number(entry.chapterNumber) !== chapterNumber
|
|
|| Number(entry.pageNumber) !== pageNumber
|
|
|| entry.assetId !== assetId
|
|
|| !Number.isFinite(durationSeconds)
|
|
|| durationSeconds <= 0
|
|
) {
|
|
return null
|
|
}
|
|
|
|
const asset = releaseAssets && releaseAssets[assetId]
|
|
const sha256 = asset && typeof asset.sha256 === 'string'
|
|
? asset.sha256
|
|
: ''
|
|
const remotePath = asset && asset.remotePath
|
|
const expectedRemotePath = (
|
|
`audio/${stablePageId.slice(0, 7).toLowerCase()}`
|
|
+ `/${stablePageId}.${sha256.slice(0, 12)}.mp3`
|
|
)
|
|
if (
|
|
!asset
|
|
|| asset.kind !== 'audio'
|
|
|| clean(asset.reviewStatus).toLowerCase() !== 'approved'
|
|
|| asset.localSeed !== ''
|
|
|| !SHA256_PATTERN.test(sha256)
|
|
|| typeof remotePath !== 'string'
|
|
|| remotePath !== expectedRemotePath
|
|
) {
|
|
return null
|
|
}
|
|
|
|
return { ...entry, assetId }
|
|
}
|
|
|
|
module.exports = {
|
|
REMOTE_PAGE_AUDIO_ID_PATTERN,
|
|
REMOTE_PAGE_AUDIO_MANIFEST_VERSION,
|
|
getApprovedRemotePageAudio,
|
|
remotePageAudioAssetId,
|
|
remotePageAudioPages,
|
|
}
|