forked from miao-moe/QZMusic_PC
feat: 实现功能&优化
- 底部播放栏 - MediaSession - 插件系统&存储位置 - URL缓存机制 - 整理项目结构
This commit is contained in:
@@ -2,8 +2,9 @@ import { app, BrowserWindow, Menu, ipcMain } from 'electron'
|
||||
import { createRequire } from 'node:module'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import { MpvController } from './mpvController'
|
||||
|
||||
import { PluginSystem } from '../src/main/pluginSystem.ts'
|
||||
// @ts-ignore
|
||||
const require = createRequire(import.meta.url)
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
@@ -45,7 +46,7 @@ function createWindow() {
|
||||
} else {
|
||||
win.loadFile(path.join(RENDERER_DIST, 'index.html'))
|
||||
}
|
||||
|
||||
win.webContents.openDevTools();
|
||||
registerZoomShortcuts(win)
|
||||
}
|
||||
|
||||
@@ -69,9 +70,25 @@ ipcMain.handle('mpv-play', () => mpv?.play())
|
||||
ipcMain.handle('mpv-pause', () => mpv?.pause())
|
||||
ipcMain.handle('mpv-toggle-pause', () => mpv?.togglePause())
|
||||
ipcMain.handle('mpv-stop', () => mpv?.stop())
|
||||
ipcMain.handle('mpv-set-volume', (e, vol) => mpv?.setVolume(vol))
|
||||
ipcMain.handle('mpv-set-volume', (_, vol) => mpv?.setVolume(vol))
|
||||
ipcMain.handle('mpv-seek', (_, time) => mpv?.seek(time))
|
||||
|
||||
// PluginSystem
|
||||
ipcMain.handle(
|
||||
'plugin:call',
|
||||
async (_evenv, pluginId: string, method: string, args: any[]) => {
|
||||
const plugin = new PluginSystem(pluginId)
|
||||
|
||||
if (typeof (plugin as any)[method] !== 'function') {
|
||||
return {
|
||||
success: false,
|
||||
error: `Method ${method} not found`
|
||||
}
|
||||
}
|
||||
|
||||
return await (plugin as any)[method](...args)
|
||||
}
|
||||
)
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
@@ -115,6 +132,34 @@ app.on('activate', () => {
|
||||
})
|
||||
|
||||
app.whenReady().then(() => {
|
||||
// Ensure plugins directory exists
|
||||
const pluginsPath = path.join(app.getPath('userData'), 'plugins')
|
||||
if (!fs.existsSync(pluginsPath)) {
|
||||
fs.mkdirSync(pluginsPath, { recursive: true })
|
||||
}
|
||||
|
||||
// --- Ensure Sample 'wy' Plugin Exists ---
|
||||
const wyPluginPath = path.join(pluginsPath, 'wy')
|
||||
const wyPluginIndex = path.join(wyPluginPath, 'index.js')
|
||||
if (!fs.existsSync(wyPluginIndex)) {
|
||||
if (!fs.existsSync(wyPluginPath)) fs.mkdirSync(wyPluginPath, { recursive: true })
|
||||
fs.writeFileSync(wyPluginIndex, `
|
||||
module.exports = {
|
||||
async getUrl(id, quality) {
|
||||
const url = \`https://api.qz.shiqianjiang.cn/music/url?source=wy&songId=\${id}&quality=\${quality}&key=testkey\`;
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (e) {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
`.trim())
|
||||
}
|
||||
// ----------------------------------------
|
||||
|
||||
Menu.setApplicationMenu(null)
|
||||
createWindow()
|
||||
|
||||
|
||||
173
electron/mpvController.ts
Normal file
173
electron/mpvController.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { Socket } from 'net';
|
||||
import { EventEmitter } from 'events';
|
||||
import path from 'path';
|
||||
|
||||
export class MpvController extends EventEmitter {
|
||||
private process: ChildProcess | null = null;
|
||||
private socket: Socket | null = null;
|
||||
private ipcPath: string;
|
||||
private messageBuffer: string = '';
|
||||
|
||||
constructor(ipcPath?: string) {
|
||||
super();
|
||||
this.ipcPath = ipcPath || this.getIpcPath();
|
||||
}
|
||||
|
||||
private getIpcPath(): string {
|
||||
if (process.platform === 'win32') {
|
||||
return '\\\\.\\pipe\\qzmusic_mpv_socket';
|
||||
}
|
||||
return '/tmp/qzmusic_mpv_socket';
|
||||
}
|
||||
|
||||
private getMpvPath(): string {
|
||||
const appRoot = process.env.APP_ROOT || process.cwd();
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
return path.join(appRoot, 'core', 'mpv.exe');
|
||||
}
|
||||
// Darwin (Mac) or Linux
|
||||
// For now, assuming global mpv or a specific path in future
|
||||
return 'mpv';
|
||||
}
|
||||
|
||||
start() {
|
||||
const mpvPath = this.getMpvPath();
|
||||
console.log('Starting MPV from:', mpvPath);
|
||||
|
||||
this.process = spawn(mpvPath, [
|
||||
'--idle',
|
||||
'--force-window=no',
|
||||
'--no-media-controls',
|
||||
`--input-ipc-server=${this.ipcPath}`,
|
||||
'--no-terminal'
|
||||
]);
|
||||
|
||||
this.process.on('error', (err) => {
|
||||
console.error('Failed to start MPV:', err);
|
||||
this.emit('error', err);
|
||||
});
|
||||
|
||||
this.process.on('exit', (code, signal) => {
|
||||
console.log(`MPV exited with code ${code} and signal ${signal}`);
|
||||
this.emit('exit', { code, signal });
|
||||
this.socket?.destroy();
|
||||
});
|
||||
|
||||
this.tryConnect();
|
||||
}
|
||||
|
||||
private tryConnect(retries = 10) {
|
||||
if (retries <= 0) {
|
||||
console.error('Could not connect to MPV socket after multiple attempts.');
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.socket = new Socket();
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
console.log('Connected to MPV IPC socket');
|
||||
this.emit('ready');
|
||||
|
||||
this.send(['observe_property', 1, 'pause']);
|
||||
this.send(['observe_property', 2, 'time-pos']);
|
||||
this.send(['observe_property', 3, 'duration']);
|
||||
this.send(['observe_property', 4, 'idle-active']);
|
||||
this.send(['observe_property', 5, 'eof-reached']);
|
||||
});
|
||||
|
||||
this.socket.on('data', (data) => {
|
||||
this.handleData(data);
|
||||
});
|
||||
|
||||
this.socket.on('error', (err) => {
|
||||
this.socket?.destroy();
|
||||
this.tryConnect(retries - 1);
|
||||
});
|
||||
|
||||
this.socket.connect(this.ipcPath);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private handleData(data: Buffer) {
|
||||
// Determine message boundaries by newline
|
||||
const raw = data.toString();
|
||||
// console.log('[MPV RX RAW]', raw.trim()); // Very noisy if enabled
|
||||
|
||||
this.messageBuffer += raw;
|
||||
const messages = this.messageBuffer.split('\n');
|
||||
|
||||
// The last part might be an incomplete message, save it back to buffer
|
||||
this.messageBuffer = messages.pop() || '';
|
||||
|
||||
for (const msg of messages) {
|
||||
if (!msg.trim()) continue;
|
||||
console.log('[IPC]', msg); // User requested raw communication
|
||||
try {
|
||||
const json = JSON.parse(msg);
|
||||
this.emit('message', json);
|
||||
|
||||
// Handle specific events
|
||||
if (json.event) {
|
||||
this.emit('event', json);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse MPV message:', msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async send(command: any[]) {
|
||||
if (!this.socket || this.socket.destroyed) {
|
||||
console.warn('MPV socket not connected');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = JSON.stringify({ command });
|
||||
console.log('[MPV TX]', payload); // User requested raw communication
|
||||
this.socket.write(payload + '\n');
|
||||
}
|
||||
|
||||
// Convenience methods
|
||||
async load(url: string) {
|
||||
return this.send(['loadfile', url]);
|
||||
}
|
||||
|
||||
async play() {
|
||||
return this.send(['set_property', 'pause', false]);
|
||||
}
|
||||
|
||||
async pause() {
|
||||
return this.send(['set_property', 'pause', true]);
|
||||
}
|
||||
|
||||
async togglePause() {
|
||||
return this.send(['cycle', 'pause']);
|
||||
}
|
||||
|
||||
async stop() {
|
||||
return this.send(['stop']);
|
||||
}
|
||||
|
||||
async setVolume(vol: number) {
|
||||
return this.send(['set_property', 'volume', vol]);
|
||||
}
|
||||
|
||||
async seek(seconds: number) {
|
||||
return this.send(['seek', seconds, 'absolute']);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.process) {
|
||||
console.log('Killing MPV process...');
|
||||
this.process.kill();
|
||||
this.process = null;
|
||||
}
|
||||
if (this.socket) {
|
||||
this.socket.destroy();
|
||||
this.socket = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,5 +17,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
setVolume: (vol: number) => ipcRenderer.invoke('mpv-set-volume', vol),
|
||||
seek: (time: number) => ipcRenderer.invoke('mpv-seek', time),
|
||||
onEvent: (callback: (event: any, data: any) => void) => ipcRenderer.on('mpv-event', callback)
|
||||
},
|
||||
|
||||
// Plugin System
|
||||
plugin: {
|
||||
call: (pluginId: string, method: string, args: any[]) => ipcRenderer.invoke('plugin:call', pluginId, method, args)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user