修复了在Windows上使用Web访问出现找不到/tmp文件的错误,现在全部都流式放到内存当中了。
并且支持本地自签名部署了
This commit is contained in:
+91
-56
@@ -13,8 +13,10 @@ export default function App() {
|
||||
const [outputHandle, setOutputHandle] = useState<any>(null)
|
||||
const [fileItems, setFileItems] = useState<FileItem[]>([])
|
||||
const [converting, setConverting] = useState(false)
|
||||
const [concurrency, setConcurrency] = useState(3)
|
||||
const [progress, setProgress] = useState({ done: 0, total: 0 })
|
||||
const abortRef = useRef(false)
|
||||
const doneCountRef = useRef(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (isWasmSupported()) {
|
||||
@@ -47,78 +49,97 @@ export default function App() {
|
||||
setOutputHandle(handle)
|
||||
}, [])
|
||||
|
||||
const processOne = useCallback(async (
|
||||
entry: { file: File; name: string },
|
||||
idx: number,
|
||||
updateStatus: (status: FileItem['status'], extra?: Partial<FileItem>) => void
|
||||
) => {
|
||||
if (abortRef.current) return
|
||||
|
||||
try {
|
||||
updateStatus('converting')
|
||||
const buf = await entry.file.arrayBuffer()
|
||||
const result = await convertNcm(buf)
|
||||
let audio = result.data
|
||||
|
||||
let coverData: Uint8Array | undefined
|
||||
if (result.coverUrl) {
|
||||
updateStatus('fetching_cover')
|
||||
try {
|
||||
const resp = await fetch(result.coverUrl)
|
||||
if (resp.ok) {
|
||||
const coverBuf = await resp.arrayBuffer()
|
||||
coverData = new Uint8Array(coverBuf)
|
||||
}
|
||||
} catch {
|
||||
// proceed without cover
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus('writing')
|
||||
if (result.title || result.artist || result.album || coverData) {
|
||||
audio = await embedMetadata(
|
||||
audio,
|
||||
result.format,
|
||||
{ title: result.title, artist: result.artist, album: result.album },
|
||||
coverData
|
||||
)
|
||||
}
|
||||
|
||||
const base = entry.name.replace(/\.ncm$/i, '')
|
||||
const outName = `${base}.${result.format}`
|
||||
|
||||
const fileHandle = await outputHandle.getFileHandle(outName, { create: true })
|
||||
const writable = await fileHandle.createWritable()
|
||||
await writable.write(audio)
|
||||
await writable.close()
|
||||
|
||||
updateStatus('done', { format: result.format })
|
||||
} catch (e: any) {
|
||||
if (!abortRef.current) {
|
||||
updateStatus('error', { error: e.message })
|
||||
}
|
||||
}
|
||||
|
||||
doneCountRef.current++
|
||||
setProgress({ done: doneCountRef.current, total: inputFiles.size })
|
||||
}, [inputFiles, outputHandle])
|
||||
|
||||
const handleConvert = useCallback(async () => {
|
||||
if (!outputHandle || inputFiles.size === 0 || !wasmReady) return
|
||||
|
||||
abortRef.current = false
|
||||
doneCountRef.current = 0
|
||||
setConverting(true)
|
||||
setProgress({ done: 0, total: inputFiles.size })
|
||||
|
||||
const entries = Array.from(inputFiles.values())
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
if (abortRef.current) break
|
||||
let nextIdx = 0
|
||||
const workers: Promise<void>[] = []
|
||||
|
||||
const entry = entries[i]
|
||||
const makeWorker = async () => {
|
||||
while (true) {
|
||||
const i = nextIdx++
|
||||
if (i >= entries.length || abortRef.current) break
|
||||
|
||||
const setStatus = (status: FileItem['status'], extra?: Partial<FileItem>) =>
|
||||
setFileItems((prev) => prev.map((item, idx) =>
|
||||
idx === i ? { ...item, status, ...extra } : item
|
||||
))
|
||||
const setStatus = (status: FileItem['status'], extra?: Partial<FileItem>) =>
|
||||
setFileItems((prev) => prev.map((item, idx) =>
|
||||
idx === i ? { ...item, status, ...extra } : item
|
||||
))
|
||||
|
||||
setStatus('converting')
|
||||
|
||||
try {
|
||||
const buf = await entry.file.arrayBuffer()
|
||||
const result = await convertNcm(buf)
|
||||
let audio = result.data
|
||||
|
||||
// Fetch cover image if available
|
||||
let coverData: Uint8Array | undefined
|
||||
if (result.coverUrl) {
|
||||
setStatus('fetching_cover')
|
||||
try {
|
||||
const resp = await fetch(result.coverUrl)
|
||||
if (resp.ok) {
|
||||
const coverBuf = await resp.arrayBuffer()
|
||||
coverData = new Uint8Array(coverBuf)
|
||||
}
|
||||
} catch {
|
||||
// cover fetch failed, proceed without it
|
||||
}
|
||||
}
|
||||
|
||||
// Embed metadata + cover
|
||||
if (result.title || result.artist || result.album || coverData) {
|
||||
setStatus('writing')
|
||||
audio = await embedMetadata(
|
||||
audio,
|
||||
result.format,
|
||||
{ title: result.title, artist: result.artist, album: result.album },
|
||||
coverData
|
||||
)
|
||||
} else {
|
||||
setStatus('writing')
|
||||
}
|
||||
|
||||
const base = entry.name.replace(/\.ncm$/i, '')
|
||||
const outName = `${base}.${result.format}`
|
||||
|
||||
const fileHandle = await outputHandle.getFileHandle(outName, { create: true })
|
||||
const writable = await fileHandle.createWritable()
|
||||
await writable.write(audio)
|
||||
await writable.close()
|
||||
|
||||
setStatus('done', { format: result.format })
|
||||
} catch (e: any) {
|
||||
setStatus('error', { error: e.message })
|
||||
await processOne(entries[i], i, setStatus)
|
||||
}
|
||||
|
||||
setProgress({ done: i + 1, total: entries.length })
|
||||
}
|
||||
|
||||
const n = Math.min(concurrency, entries.length)
|
||||
for (let w = 0; w < n; w++) {
|
||||
workers.push(makeWorker())
|
||||
}
|
||||
|
||||
await Promise.all(workers)
|
||||
setConverting(false)
|
||||
}, [inputFiles, outputHandle, wasmReady])
|
||||
}, [inputFiles, outputHandle, wasmReady, processOne, concurrency])
|
||||
|
||||
const handleAbort = useCallback(() => {
|
||||
abortRef.current = true
|
||||
@@ -168,6 +189,20 @@ export default function App() {
|
||||
{inputFiles.size > 0 && (
|
||||
<div className="mt-3 animate-fade-in flex items-center gap-3">
|
||||
<StatusBar progress={progress} />
|
||||
<div className="flex shrink-0 items-center gap-2 text-xs text-slate-400">
|
||||
<span>并行</span>
|
||||
<select
|
||||
value={concurrency}
|
||||
onChange={(e) => setConcurrency(Number(e.target.value))}
|
||||
disabled={converting}
|
||||
className="rounded-lg bg-slate-800 border border-slate-700 px-2 py-2 text-slate-200
|
||||
focus:outline-none focus:border-indigo-500 disabled:opacity-40"
|
||||
>
|
||||
{[1, 2, 3, 4, 6, 8, 12, 16, 24, 32].map((n) => (
|
||||
<option key={n} value={n}>{n}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleConvert}
|
||||
disabled={!canConvert}
|
||||
|
||||
Reference in New Issue
Block a user