fix(local-music): 主进程实现 Range/206 让本地音乐 seek/快进恢复

切到 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,一次拿文件大小且过滤掉目录。
This commit is contained in:
chengww
2026-05-17 20:16:25 +08:00
parent 51f1aaba55
commit c15a10c098
+44 -5
View File
@@ -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://) 在当前版本不会回 206audio 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 });