mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-07-12 21:43:23 +08:00
Merge pull request #675 from chengww5217/fix/local-music-playback
fix(local-music): 修复本地音乐播放失败、进度异常与切歌失效
This commit is contained in:
@@ -46,5 +46,8 @@ AGENTS.md
|
|||||||
.auto-imports.d.ts
|
.auto-imports.d.ts
|
||||||
.components.d.ts
|
.components.d.ts
|
||||||
|
|
||||||
|
# TypeScript 增量编译缓存
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
src/renderer/auto-imports.d.ts
|
src/renderer/auto-imports.d.ts
|
||||||
src/renderer/components.d.ts
|
src/renderer/components.d.ts
|
||||||
|
|||||||
+18
-1
@@ -1,7 +1,24 @@
|
|||||||
import { electronApp, optimizer } from '@electron-toolkit/utils';
|
import { electronApp, optimizer } from '@electron-toolkit/utils';
|
||||||
import { app, ipcMain, nativeImage, session } from 'electron';
|
import { app, ipcMain, nativeImage, protocol, session } from 'electron';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
|
// 必须在 app.whenReady() 之前注册自定义协议为特权协议,
|
||||||
|
// 否则 http(s) 页面(dev server、生产环境的 file://)无法把 local:// 当成
|
||||||
|
// 安全/可 fetch/可流式的资源加载,会触发 CORS 拦截或 net::ERR_UNKNOWN_URL_SCHEME
|
||||||
|
protocol.registerSchemesAsPrivileged([
|
||||||
|
{
|
||||||
|
scheme: 'local',
|
||||||
|
privileges: {
|
||||||
|
standard: true,
|
||||||
|
secure: true,
|
||||||
|
supportFetchAPI: true,
|
||||||
|
stream: true,
|
||||||
|
bypassCSP: true,
|
||||||
|
corsEnabled: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
import type { Language } from '../i18n/main';
|
import type { Language } from '../i18n/main';
|
||||||
import i18n from '../i18n/main';
|
import i18n from '../i18n/main';
|
||||||
import { loadLyricWindow } from './lyric';
|
import { loadLyricWindow } from './lyric';
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { app, dialog, ipcMain, protocol, shell } from 'electron';
|
|||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
import { getStore } from './config';
|
import { getStore } from './config';
|
||||||
|
|
||||||
@@ -23,36 +24,79 @@ function sanitizeFilename(filename: string): string {
|
|||||||
.trim();
|
.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监听
|
* 初始化文件管理相关的IPC监听
|
||||||
*/
|
*/
|
||||||
export function initializeFileManager() {
|
export function initializeFileManager() {
|
||||||
// 注册本地文件协议
|
// 注册本地文件协议
|
||||||
protocol.registerFileProtocol('local', (request, callback) => {
|
// Electron 25+ 起 registerFileProtocol 已弃用,改用 protocol.handle,并配合 main/index.ts
|
||||||
|
// 中的 registerSchemesAsPrivileged,让 audio 元素能从 http(s) 页面跨协议加载本地文件
|
||||||
|
protocol.handle('local', async (request) => {
|
||||||
try {
|
try {
|
||||||
const url = request.url;
|
// local:///<absolute-path>
|
||||||
// local://C:/Users/xxx.mp3
|
let filePath = decodeURIComponent(request.url.replace(/^local:\/\/\/?/, ''));
|
||||||
let filePath = decodeURIComponent(url.replace('local:///', ''));
|
|
||||||
|
|
||||||
// 兼容 local:///C:/Users/xxx.mp3 这种情况
|
// Windows: 协议解析后可能是 /C:/...,去掉前导斜杠
|
||||||
if (/^\/[a-zA-Z]:\//.test(filePath)) {
|
if (/^\/[a-zA-Z]:\//.test(filePath)) {
|
||||||
filePath = filePath.slice(1);
|
filePath = filePath.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 还原为系统路径格式
|
// macOS/Linux 上去掉前导斜杠后会丢失绝对路径标识,这里补回
|
||||||
filePath = path.normalize(filePath);
|
if (process.platform !== 'win32' && !filePath.startsWith('/')) {
|
||||||
|
filePath = '/' + filePath;
|
||||||
// 检查文件是否存在
|
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
console.error('File not found:', filePath);
|
|
||||||
callback({ error: -6 }); // net::ERR_FILE_NOT_FOUND
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
callback({ path: filePath });
|
filePath = path.normalize(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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildLocalFileResponse(filePath, stat.size, request.headers.get('range'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error handling local protocol:', error);
|
console.error('Error handling local protocol:', error);
|
||||||
callback({ error: -2 }); // net::FAILED
|
return new Response(null, { status: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
@contextmenu.prevent="handleContextMenu"
|
@contextmenu.prevent="handleContextMenu"
|
||||||
@mouseenter="handleMouseEnter"
|
@mouseenter="handleMouseEnter"
|
||||||
@mouseleave="handleMouseLeave"
|
@mouseleave="handleMouseLeave"
|
||||||
@dblclick.stop="playMusicEvent(item)"
|
@dblclick.stop="handlePlay(item)"
|
||||||
>
|
>
|
||||||
<slot name="index"></slot>
|
<slot name="index"></slot>
|
||||||
<slot name="select" v-if="selectable"></slot>
|
<slot name="select" v-if="selectable"></slot>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
:is-dislike="isDislike"
|
:is-dislike="isDislike"
|
||||||
:can-remove="canRemove"
|
:can-remove="canRemove"
|
||||||
@update:show="showDropdown = $event"
|
@update:show="showDropdown = $event"
|
||||||
@play="playMusicEvent(item)"
|
@play="handlePlay(item)"
|
||||||
@play-next="handlePlayNext"
|
@play-next="handlePlayNext"
|
||||||
@download="downloadMusic(item)"
|
@download="downloadMusic(item)"
|
||||||
@download-lyric="downloadLyric(item)"
|
@download-lyric="downloadLyric(item)"
|
||||||
@@ -83,6 +83,12 @@ const imageLoad = async (event: Event) => {
|
|||||||
await handleImageLoad(target);
|
await handleImageLoad(target);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 双击和右键菜单"播放"统一入口:先通知父组件设置播放列表上下文,再触发播放
|
||||||
|
const handlePlay = (song: SongResult) => {
|
||||||
|
emits('play', song);
|
||||||
|
playMusicEvent(song);
|
||||||
|
};
|
||||||
|
|
||||||
// 切换选择状态
|
// 切换选择状态
|
||||||
const toggleSelect = () => {
|
const toggleSelect = () => {
|
||||||
emits('select', props.item.id, !props.selected);
|
emits('select', props.item.id, !props.selected);
|
||||||
|
|||||||
Reference in New Issue
Block a user