gengxin
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 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,
|
||||
}
|
||||
Reference in New Issue
Block a user