diff --git a/package.json b/package.json index aa71f90..b01955b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "2.0.4", + "version": "2.0.5", "type": "module", "scripts": { "dev": "vite", diff --git a/server/routes/nuggets-host-upload.js b/server/routes/nuggets-host-upload.js index cd7dfa6..ab50f25 100644 --- a/server/routes/nuggets-host-upload.js +++ b/server/routes/nuggets-host-upload.js @@ -184,7 +184,7 @@ export function register(app) { res.json({ ok: true }) }) - // ── File upload ─────────────────────────────────────────────────────── + // ── Episode upload (audio file + metadata) ─────────────────────────── app.post('/api/nuggets-upload/upload', requireHostAuth, upload.single('file'), async (req, res) => { if (!req.file) { @@ -192,10 +192,19 @@ export function register(app) { return } - const targetDir = req.body?.targetDir === 'info' ? SEAFILE_EPISODE_INFO_DIR : SEAFILE_EPISODE_UPLOAD_DIR + const session = getHostSession(req) + const episodeName = typeof req.body?.episodeName === 'string' ? req.body.episodeName.trim() : '' + const passage = typeof req.body?.passage === 'string' ? req.body.passage.trim() : '' + const description = typeof req.body?.description === 'string' ? req.body.description.trim() : '' const originalName = req.file.originalname || req.file.filename const tempPath = req.file.path + if (!episodeName || !passage) { + await unlink(tempPath).catch(() => {}) + res.status(400).json({ message: 'Episode name and passage are required.' }) + return + } + if (!seafileConfigured()) { await unlink(tempPath).catch(() => {}) res.status(503).json({ message: 'Seafile is not configured on this server. Contact the admin.' }) @@ -206,14 +215,33 @@ export function register(app) { const { readFile: rf } = await import('node:fs/promises') const fileBuffer = await rf(tempPath) - const uploadLink = await getSeafileUploadLink(targetDir) - await uploadToSeafile(uploadLink, fileBuffer, originalName, targetDir) - + // Upload audio to /Episode Upload/ + const audioLink = await getSeafileUploadLink(SEAFILE_EPISODE_UPLOAD_DIR) + await uploadToSeafile(audioLink, fileBuffer, originalName, SEAFILE_EPISODE_UPLOAD_DIR) await unlink(tempPath).catch(() => {}) - const session = getHostSession(req) - console.log(`[nuggets-upload] ${session?.hostName ?? 'unknown'} uploaded "${originalName}" to ${targetDir}`) - res.json({ ok: true, filename: originalName, dir: targetDir }) + // Build and upload info file to /Episode Info/ + const now = new Date() + const dateStr = now.toISOString().slice(0, 10) + const infoContent = [ + `Episode: ${episodeName}`, + `Host: ${session?.hostName ?? 'Unknown'}`, + `Passage: ${passage}`, + `Audio File: ${originalName}`, + `Submitted: ${now.toUTCString()}`, + '', + 'Description:', + description || '(none)', + ].join('\n') + + const safeEpName = episodeName.replace(/[^a-z0-9 _-]/gi, '').trim().replace(/\s+/g, '-').slice(0, 60) + const infoFilename = `${dateStr}-${safeEpName}-info.txt` + const infoBuffer = Buffer.from(infoContent, 'utf8') + const infoLink = await getSeafileUploadLink(SEAFILE_EPISODE_INFO_DIR) + await uploadToSeafile(infoLink, infoBuffer, infoFilename, SEAFILE_EPISODE_INFO_DIR) + + console.log(`[nuggets-upload] ${session?.hostName ?? 'unknown'} uploaded "${episodeName}" (${originalName})`) + res.json({ ok: true, episodeName, filename: originalName, infoFile: infoFilename }) } catch (err) { await unlink(tempPath).catch(() => {}) console.error('[nuggets-upload] upload error:', err.message) diff --git a/src/components/NuggetsHostPortal.tsx b/src/components/NuggetsHostPortal.tsx index f689d78..90180d9 100644 --- a/src/components/NuggetsHostPortal.tsx +++ b/src/components/NuggetsHostPortal.tsx @@ -1,10 +1,8 @@ import { useState, useEffect, useRef } from 'react' -type UploadTarget = 'upload' | 'info' - -interface UploadRecord { +interface SubmittedEpisode { + episodeName: string filename: string - dir: string at: string } @@ -19,14 +17,17 @@ export function NuggetsHostPortal() { const [loginError, setLoginError] = useState('') const [loggingIn, setLoggingIn] = useState(false) - const [target, setTarget] = useState('upload') + const [episodeName, setEpisodeName] = useState('') + const [passage, setPassage] = useState('') + const [description, setDescription] = useState('') + const [file, setFile] = useState(null) const [dragging, setDragging] = useState(false) const [uploading, setUploading] = useState(false) const [progress, setProgress] = useState(0) const [uploadError, setUploadError] = useState('') - const [uploads, setUploads] = useState([]) + const [submitted, setSubmitted] = useState([]) + const fileInputRef = useRef(null) - const dropRef = useRef(null) useEffect(() => { fetch('/api/nuggets-upload/me') @@ -50,10 +51,7 @@ export function NuggetsHostPortal() { body: JSON.stringify({ username, password }), }) const d = await r.json() - if (!r.ok) { - setLoginError(d.message ?? 'Login failed.') - return - } + if (!r.ok) { setLoginError(d.message ?? 'Login failed.'); return } setHostName(d.hostName) setAuthenticated(true) } finally { @@ -67,25 +65,53 @@ export function NuggetsHostPortal() { setHostName('') setUsername('') setPassword('') - setUploads([]) + setSubmitted([]) + resetForm() } - async function uploadFile(file: File) { + function resetForm() { + setEpisodeName('') + setPassage('') + setDescription('') + setFile(null) + setUploadError('') + setProgress(0) + } + + function handleDrop(e: React.DragEvent) { + e.preventDefault() + setDragging(false) + const dropped = e.dataTransfer.files[0] + if (dropped) setFile(dropped) + } + + function handleFileChange(e: React.ChangeEvent) { + const picked = e.target.files?.[0] + if (picked) setFile(picked) + e.target.value = '' + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + if (!file) { setUploadError('Please select an audio file to upload.'); return } if (uploading) return + setUploadError('') setUploading(true) setProgress(0) const formData = new FormData() formData.append('file', file) - formData.append('targetDir', target) + formData.append('episodeName', episodeName) + formData.append('passage', passage) + formData.append('description', description) - return new Promise((resolve) => { + await new Promise(resolve => { const xhr = new XMLHttpRequest() xhr.open('POST', '/api/nuggets-upload/upload') - xhr.upload.addEventListener('progress', e => { - if (e.lengthComputable) setProgress(Math.round((e.loaded / e.total) * 100)) + xhr.upload.addEventListener('progress', ev => { + if (ev.lengthComputable) setProgress(Math.round((ev.loaded / ev.total) * 100)) }) xhr.addEventListener('load', () => { @@ -93,15 +119,14 @@ export function NuggetsHostPortal() { if (xhr.status === 200) { try { const d = JSON.parse(xhr.responseText) - setUploads(prev => [{ filename: d.filename, dir: d.dir, at: new Date().toLocaleTimeString() }, ...prev]) - setProgress(100) + setSubmitted(prev => [{ episodeName: d.episodeName, filename: d.filename, at: new Date().toLocaleTimeString() }, ...prev]) + resetForm() } catch { setUploadError('Upload succeeded but response was unexpected.') } } else { try { - const d = JSON.parse(xhr.responseText) - setUploadError(d.message ?? 'Upload failed.') + setUploadError(JSON.parse(xhr.responseText).message ?? 'Upload failed.') } catch { setUploadError('Upload failed.') } @@ -119,24 +144,11 @@ export function NuggetsHostPortal() { }) } - function handleDrop(e: React.DragEvent) { - e.preventDefault() - setDragging(false) - const file = e.dataTransfer.files[0] - if (file) uploadFile(file) - } - - function handleFileChange(e: React.ChangeEvent) { - const file = e.target.files?.[0] - if (file) uploadFile(file) - e.target.value = '' - } - if (checking) { return ( -
-
-

Loading…

+
+
+

Loading…

) @@ -144,44 +156,27 @@ export function NuggetsHostPortal() { if (!authenticated) { return ( -
-
-
- 🎙️ - Nuggets +
+
+
+ 🎙️ + Nuggets
-

Host Upload Portal

-

Sign in to upload your episode files to Seafile.

- -
-
- - setUsername(e.target.value)} - disabled={loggingIn} - required - /> +

Host Upload Portal

+

Sign in with your host credentials to submit an episode.

+ +
+ + setUsername(e.target.value)} disabled={loggingIn} required />
-
- - setPassword(e.target.value)} - disabled={loggingIn} - required - /> +
+ + setPassword(e.target.value)} disabled={loggingIn} required />
- {loginError &&

{loginError}

} - @@ -190,98 +185,140 @@ export function NuggetsHostPortal() { ) } - const targetLabel = target === 'upload' ? 'Episode Upload' : 'Episode Info' - const acceptTypes = target === 'upload' - ? '.mp3,.wav,.m4a,.aac,.flac,.ogg,.mp4,.mov' - : '*' - return ( -
-
-
-
- 🎙️ - Nuggets Upload Portal +
+
+ {/* Header */} +
+
+ 🎙️ + Nuggets Upload Portal
-
- Hi, {hostName} - +
+ {hostName} +
{!seafileConfigured && ( -
- Seafile is not configured yet. Contact the admin to set up the connection. +
+ Seafile is not configured yet — contact the admin before uploading.
)} -
- - -
+
+ {/* Episode Name */} +
+ + setEpisodeName(e.target.value)} + disabled={uploading} + required + /> +
-

- {target === 'upload' - ? 'Upload your finished audio file (MP3, WAV, M4A, etc.).' - : 'Upload show notes, outlines, scripts, or any supporting document.'} -

+ {/* Passage */} +
+ + setPassage(e.target.value)} + disabled={uploading} + required + /> +
-
{ e.preventDefault(); setDragging(true) }} - onDragLeave={() => setDragging(false)} - onDrop={handleDrop} - onClick={() => !uploading && fileInputRef.current?.click()} - > - - {uploading ? ( -
-

Uploading to {targetLabel}…

-
-
+ {/* Description */} +
+ +