Fix Nuggets workflow audio: serve with audio content type (nosniff blocked octet-stream), show playback errors; v2.17.1
Publish Container / build (push) Successful in 16m34s
Publish Container / build (push) Successful in 16m34s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
326838e5bc
commit
11f3d76e9b
4 files changed
+34
-7
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"version": "2.17.0",
|
"version": "2.17.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"version": "2.17.0",
|
"version": "2.17.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.1135.0",
|
"@aws-sdk/client-s3": "^3.1135.0",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.17.0",
|
"version": "2.17.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -15,6 +15,15 @@ import {
|
|||||||
|
|
||||||
const WORKFLOW_FILE = path.join(DATA_DIR, 'nuggets-workflow.json')
|
const WORKFLOW_FILE = path.join(DATA_DIR, 'nuggets-workflow.json')
|
||||||
|
|
||||||
|
const AUDIO_TYPES = {
|
||||||
|
'.mp3': 'audio/mpeg',
|
||||||
|
'.wav': 'audio/wav',
|
||||||
|
'.m4a': 'audio/mp4',
|
||||||
|
'.aac': 'audio/aac',
|
||||||
|
'.ogg': 'audio/ogg',
|
||||||
|
'.flac': 'audio/flac',
|
||||||
|
}
|
||||||
|
|
||||||
export const WORKFLOW_STEPS = ['listen', 'coldOpen', 'recordColdOpen', 'edit', 'spotify']
|
export const WORKFLOW_STEPS = ['listen', 'coldOpen', 'recordColdOpen', 'edit', 'spotify']
|
||||||
|
|
||||||
async function loadWorkflow() {
|
async function loadWorkflow() {
|
||||||
@@ -100,6 +109,7 @@ export function register(app) {
|
|||||||
{ headers: { Authorization: `Token ${SEAFILE_TOKEN}` }, signal: AbortSignal.timeout(10_000) },
|
{ headers: { Authorization: `Token ${SEAFILE_TOKEN}` }, signal: AbortSignal.timeout(10_000) },
|
||||||
)
|
)
|
||||||
if (!linkRes.ok) {
|
if (!linkRes.ok) {
|
||||||
|
console.error(`[nuggets-workflow] Seafile link lookup for "${filePath}" failed: ${linkRes.status}`)
|
||||||
res.status(404).json({ message: 'Audio file not found in Seafile.' })
|
res.status(404).json({ message: 'Audio file not found in Seafile.' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -109,21 +119,26 @@ export function register(app) {
|
|||||||
if (req.headers.range) headers.Range = req.headers.range
|
if (req.headers.range) headers.Range = req.headers.range
|
||||||
const upstream = await fetch(downloadUrl, { headers })
|
const upstream = await fetch(downloadUrl, { headers })
|
||||||
if (!upstream.ok && upstream.status !== 206) {
|
if (!upstream.ok && upstream.status !== 206) {
|
||||||
|
console.error(`[nuggets-workflow] Seafile download failed: ${upstream.status}`)
|
||||||
res.status(502).json({ message: 'Could not fetch audio from Seafile.' })
|
res.status(502).json({ message: 'Could not fetch audio from Seafile.' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res.status(upstream.status)
|
res.status(upstream.status)
|
||||||
for (const h of ['content-type', 'content-length', 'content-range', 'accept-ranges']) {
|
for (const h of ['content-length', 'content-range', 'accept-ranges']) {
|
||||||
const v = upstream.headers.get(h)
|
const v = upstream.headers.get(h)
|
||||||
if (v) res.setHeader(h, v)
|
if (v) res.setHeader(h, v)
|
||||||
}
|
}
|
||||||
if (!upstream.headers.get('content-type')) res.setHeader('content-type', 'audio/mpeg')
|
// Seafile serves downloads as application/octet-stream, which browsers refuse to play
|
||||||
|
// (the site sends nosniff), so set the audio type from the file extension.
|
||||||
|
const ext = path.extname(entry.filename).toLowerCase()
|
||||||
|
res.setHeader('content-type', AUDIO_TYPES[ext] ?? 'audio/mpeg')
|
||||||
|
if (!res.getHeader('accept-ranges')) res.setHeader('accept-ranges', 'bytes')
|
||||||
const stream = Readable.fromWeb(upstream.body)
|
const stream = Readable.fromWeb(upstream.body)
|
||||||
req.on('close', () => stream.destroy())
|
req.on('close', () => stream.destroy())
|
||||||
stream.on('error', () => res.end())
|
stream.on('error', () => res.end())
|
||||||
stream.pipe(res)
|
stream.pipe(res)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[nuggets-workflow] audio error:', err.message)
|
console.error('[nuggets-workflow] audio error:', err.message, err.cause?.message ?? '')
|
||||||
if (!res.headersSent) res.status(500).json({ message: 'Audio stream failed.' })
|
if (!res.headersSent) res.status(500).json({ message: 'Audio stream failed.' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+13
-1
@@ -1751,7 +1751,19 @@ function NuggetsWorkflowAdmin() {
|
|||||||
|
|
||||||
{s.key === 'listen' && (
|
{s.key === 'listen' && (
|
||||||
<div style={{ marginTop: 8 }}>
|
<div style={{ marginTop: 8 }}>
|
||||||
<audio controls preload="none" style={{ width: '100%' }} src={`/api/admin/nuggets-workflow/${selected.id}/audio`} />
|
<audio
|
||||||
|
key={selected.id}
|
||||||
|
controls
|
||||||
|
preload="none"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
src={`/api/admin/nuggets-workflow/${selected.id}/audio`}
|
||||||
|
onError={async () => {
|
||||||
|
const r = await fetch(`/api/admin/nuggets-workflow/${selected.id}/audio`, { headers: { Range: 'bytes=0-0' } }).catch(() => null)
|
||||||
|
if (!r) setError('Audio request failed (network).')
|
||||||
|
else if (r.ok) setError('The server sent the audio but the browser could not play it. The file may be in an unsupported format.')
|
||||||
|
else setError(`Audio unavailable (${r.status}): ${(await r.json().catch(() => ({}))).message ?? 'unknown error'}`)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{s.key === 'coldOpen' && (
|
{s.key === 'coldOpen' && (
|
||||||
|
|||||||
Reference in new issue
Block a user