From c15a10c0983879731fc692638325f0c7fcf9d33b Mon Sep 17 00:00:00 2001 From: chengww Date: Sun, 17 May 2026 20:16:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(local-music):=20=E4=B8=BB=E8=BF=9B=E7=A8=8B?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20Range/206=20=E8=AE=A9=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E9=9F=B3=E4=B9=90=20seek/=E5=BF=AB=E8=BF=9B=E6=81=A2=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 切到 protocol.handle 后,audio 元素发的 Range 请求会带 HTTP 头到达 主进程;net.fetch(file://) 既不识别 Range 也不带 Accept-Ranges,导致 浏览器认为 local:// 不支持随机访问,seek 时只能从头加载,表现为 快进失效、退出后恢复播放从 0 开始。 新增 buildLocalFileResponse: - 无 Range:200 + Accept-Ranges: bytes,让 audio 知道支持随机访问 - 有 Range:206 + Content-Range,用 fs.createReadStream 切片返回 - 非法 Range:416 + Content-Range: bytes */total 顺手把 fs.existsSync 换成 stat,一次拿文件大小且过滤掉目录。 --- src/main/modules/fileManager.ts | 49 +++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/src/main/modules/fileManager.ts b/src/main/modules/fileManager.ts index 45ae3ef..b3da081 100644 --- a/src/main/modules/fileManager.ts +++ b/src/main/modules/fileManager.ts @@ -1,8 +1,8 @@ -import { app, dialog, ipcMain, net, protocol, shell } from 'electron'; +import { app, dialog, ipcMain, protocol, shell } from 'electron'; import Store from 'electron-store'; import * as fs from 'fs'; import * as path from 'path'; -import { pathToFileURL } from 'url'; +import { Readable } from 'stream'; import { getStore } from './config'; @@ -24,6 +24,45 @@ function sanitizeFilename(filename: string): string { .trim(); } +// Electron net.fetch(file://) 在当前版本不会回 206,audio seek 需要主进程自己处理 Range +function buildLocalFileResponse( + filePath: string, + total: number, + rangeHeader: string | null +): Response { + const range416 = () => + new Response(null, { status: 416, headers: { 'Content-Range': `bytes */${total}` } }); + + let start = 0; + let end = total - 1; + let partial = false; + + if (rangeHeader) { + const m = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim()); + if (!m || (!m[1] && !m[2])) return range416(); + if (m[1]) { + start = parseInt(m[1], 10); + if (m[2]) end = Math.min(parseInt(m[2], 10), end); + } else { + start = Math.max(0, total - parseInt(m[2], 10)); + } + if (start > end || start >= total) return range416(); + partial = true; + } + + return new Response( + Readable.toWeb(fs.createReadStream(filePath, { start, end })) as ReadableStream, + { + status: partial ? 206 : 200, + headers: { + 'Content-Length': String(end - start + 1), + 'Accept-Ranges': 'bytes', + ...(partial && { 'Content-Range': `bytes ${start}-${end}/${total}` }) + } + } + ); +} + /** * 初始化文件管理相关的IPC监听 */ @@ -48,13 +87,13 @@ export function initializeFileManager() { filePath = path.normalize(filePath); - if (!fs.existsSync(filePath)) { + const stat = await fs.promises.stat(filePath).catch(() => null); + if (!stat?.isFile()) { console.error('File not found:', filePath); return new Response(null, { status: 404 }); } - // 用 net.fetch 走 file:// 加载,自动支持 Range / streaming,让媒体元素能正常 seek - return net.fetch(pathToFileURL(filePath).toString()); + return buildLocalFileResponse(filePath, stat.size, request.headers.get('range')); } catch (error) { console.error('Error handling local protocol:', error); return new Response(null, { status: 500 });