mirror of
https://github.com/lqtmcstudio/QZMusic_PC.git
synced 2026-06-20 23:35:06 +08:00
fork(fix): Clone AMLL 并修复 BUG
- 将AMLL Clone到本以地进行修复和优化(emm虽然这很不优雅但是暂时无时间做子模块和Fork) - 修复在当前播放歌词行不可见的视口Seek会出现滚动偏移的问题
This commit is contained in:
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import { QzpController } from './qzpController'
|
||||
import { startProxyServer, cleanupCache, getCacheDir, getCacheSize, setPersistCache, clearCacheNow } from './proxyServer'
|
||||
import { startProxyServer, cleanupCache, getCacheDir, getCacheSize, setPersistCache, clearCacheNow, refreshCacheDir } from './proxyServer'
|
||||
import { PluginSystem } from './pluginSystem'
|
||||
import { loadSettings, saveSettings, getSetting, AppSettings } from './settingsStore'
|
||||
|
||||
@@ -202,23 +202,8 @@ ipcMain.handle('cache:changeLocation', async (_, newPath: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Update Settings
|
||||
saveSettings({ cachePath: newPath })
|
||||
|
||||
// 4. Update Proxy Server if needed (it reads from settings or we notify it)
|
||||
// Ideally proxyServer should just use `getCacheDir()` which now reads from settings?
|
||||
// Wait, `getCacheDir` in proxyServer.ts probably uses hardcoded or internal variable.
|
||||
// We need to update proxyServer.ts logic too or restart it.
|
||||
// For now, let's assume getCacheDir() needs update or we restart proxy.
|
||||
// Actually, let's make sure proxyServer gets the new path.
|
||||
// We'll restart proxy to be safe or add a setPath method.
|
||||
// *Self-correction*: The simple way is to restart the proxy server function implies checking `getCacheDir` from settings.
|
||||
// Let's ensure proxyServer.ts `setCacheDir` exists or similar.
|
||||
|
||||
// RE-CHECK: `proxyServer.ts` isn't fully visible here.
|
||||
// I'll add a TODO/Warning in comments and handle proxy update via current imports if possible.
|
||||
// Since I can't `import { setCachePath } from './proxyServer'` yet (I haven't checked if it exists),
|
||||
// I will trust that the next step or automatic reload handles it, OR better: implement `updateCachePath` in proxyServer.
|
||||
refreshCacheDir()
|
||||
|
||||
return { success: true, message: 'Cache location updated', path: newPath }
|
||||
} catch (e: any) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,29 @@
|
||||
//@ts-ignore
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { Socket } from 'net';
|
||||
import { EventEmitter } from 'events';
|
||||
import path from 'path';
|
||||
import { spawn, type ChildProcess } from 'node:child_process';
|
||||
import { Socket } from 'node:net';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import path from 'node:path';
|
||||
import { app } from 'electron';
|
||||
import { MessagePlugin } from "tdesign-vue-next";
|
||||
|
||||
export class QzpController extends EventEmitter {
|
||||
private process: ChildProcess | null = null;
|
||||
private socket: Socket | null = null;
|
||||
private ipcPath: string;
|
||||
private messageBuffer: string = '';
|
||||
private readonly ipcPath: string;
|
||||
private messageBuffer = '';
|
||||
private connectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private restartTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private pendingCommands: any[][] = [];
|
||||
private currentUrl: string | null = null;
|
||||
private lastKnownTime = 0;
|
||||
private desiredPause = true;
|
||||
private volume = 50;
|
||||
private isConnected = false;
|
||||
private isRestarting = false;
|
||||
private isShuttingDown = false;
|
||||
|
||||
private readonly connectRetryDelay = 250;
|
||||
private readonly maxConnectRetries = 20;
|
||||
private readonly maxPendingCommands = 50;
|
||||
private readonly restartDelay = 150;
|
||||
|
||||
constructor(ipcPath?: string) {
|
||||
super();
|
||||
@@ -32,138 +45,341 @@ export class QzpController extends EventEmitter {
|
||||
return path.join(appRoot, 'core', 'qzplayer.exe');
|
||||
}
|
||||
|
||||
start() {
|
||||
start(): void {
|
||||
if (this.process || this.connectTimer || this.isRestarting) return;
|
||||
|
||||
this.isShuttingDown = false;
|
||||
this.messageBuffer = '';
|
||||
|
||||
const playerPath = this.getCorePath();
|
||||
console.log('Starting QZPlayer from:', playerPath);
|
||||
|
||||
this.process = spawn(playerPath);
|
||||
try {
|
||||
const child = spawn(playerPath, [], {
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
});
|
||||
this.process = child;
|
||||
|
||||
this.process.on('error', (err) => {
|
||||
console.error('Failed to start QZPlayer:', err);
|
||||
MessagePlugin.error(`启动播放核心时出现异常:${err}`).then();
|
||||
this.emit('error', err);
|
||||
});
|
||||
child.once('error', (err) => {
|
||||
if (this.process === child) this.process = null;
|
||||
console.error('Failed to start QZPlayer:', err);
|
||||
this.emitError(err);
|
||||
this.restart(`process error: ${err.message}`);
|
||||
});
|
||||
|
||||
this.process.on('exit', (code, signal) => {
|
||||
console.log(`QZPlayer exited with code ${code} and signal ${signal}`);
|
||||
//MessagePlugin.error(`播放核心异常退出!code:${code},signal:${signal}`).then();
|
||||
this.emit('exit', { code, signal });
|
||||
this.socket?.destroy();
|
||||
});
|
||||
child.once('exit', (code, signal) => {
|
||||
if (this.process === child) this.process = null;
|
||||
console.log(`QZPlayer exited with code ${code} and signal ${signal}`);
|
||||
this.emit('exit', { code, signal });
|
||||
this.destroySocket();
|
||||
this.restart(`process exited: code=${code}, signal=${signal}`);
|
||||
});
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
console.error('Failed to spawn QZPlayer:', error);
|
||||
this.emitError(error);
|
||||
this.restart(`spawn failed: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.tryConnect();
|
||||
}
|
||||
|
||||
private tryConnect(retries = 10) {
|
||||
if (retries <= 0) {
|
||||
console.error('Could not connect to QZPlayer socket after multiple attempts.');
|
||||
return;
|
||||
}
|
||||
private tryConnect(retries = this.maxConnectRetries): void {
|
||||
if (this.isShuttingDown || this.isRestarting) return;
|
||||
|
||||
setTimeout(() => {
|
||||
this.socket = new Socket();
|
||||
this.clearConnectTimer();
|
||||
this.connectTimer = setTimeout(() => {
|
||||
this.connectTimer = null;
|
||||
if (this.isShuttingDown || this.isRestarting) return;
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
const socket = new Socket();
|
||||
let connected = false;
|
||||
let handledConnectFailure = false;
|
||||
this.socket = socket;
|
||||
|
||||
socket.once('connect', () => {
|
||||
connected = true;
|
||||
this.isConnected = true;
|
||||
this.messageBuffer = '';
|
||||
console.log('Connected to QZPlayer 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.send(['set_property', 'volume', 50])
|
||||
this.initPlayerState();
|
||||
this.restorePlaybackState();
|
||||
this.flushPendingCommands();
|
||||
});
|
||||
|
||||
this.socket.on('data', (data) => {
|
||||
socket.on('data', (data) => {
|
||||
this.handleData(data);
|
||||
});
|
||||
|
||||
this.socket.on('error', (_) => {
|
||||
this.socket?.destroy();
|
||||
this.tryConnect(retries - 1);
|
||||
socket.once('error', (err) => {
|
||||
if (!connected) {
|
||||
handledConnectFailure = true;
|
||||
this.destroySocket(socket);
|
||||
if (retries > 1) {
|
||||
this.tryConnect(retries - 1);
|
||||
} else {
|
||||
this.restart(`socket connect failed: ${err.message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn('QZPlayer IPC socket error:', err);
|
||||
this.restart(`socket error: ${err.message}`);
|
||||
});
|
||||
|
||||
this.socket.connect(this.ipcPath);
|
||||
}, 500);
|
||||
socket.once('close', () => {
|
||||
if (this.socket === socket) {
|
||||
this.socket = null;
|
||||
this.isConnected = false;
|
||||
}
|
||||
|
||||
if (this.isShuttingDown || this.isRestarting) return;
|
||||
|
||||
if (connected) {
|
||||
this.restart('socket closed');
|
||||
} else if (!handledConnectFailure) {
|
||||
if (retries > 1) {
|
||||
this.tryConnect(retries - 1);
|
||||
} else {
|
||||
this.restart('socket closed before connect');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.connect(this.ipcPath);
|
||||
}, this.connectRetryDelay);
|
||||
}
|
||||
|
||||
private handleData(data: Buffer) {
|
||||
// Determine message boundaries by newline
|
||||
private initPlayerState(): void {
|
||||
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.send(['set_property', 'volume', this.volume]);
|
||||
}
|
||||
|
||||
private restorePlaybackState(): void {
|
||||
const hasPendingLoad = this.pendingCommands.some((command) => command[0] === 'loadfile');
|
||||
if (!this.currentUrl || hasPendingLoad) return;
|
||||
|
||||
const restoreTime = this.lastKnownTime;
|
||||
this.send(['loadfile', this.currentUrl]);
|
||||
if (restoreTime > 0) {
|
||||
this.send(['seek', restoreTime, 'absolute']);
|
||||
}
|
||||
this.send(['set_property', 'pause', this.desiredPause]);
|
||||
}
|
||||
|
||||
private restart(reason: string): void {
|
||||
if (this.isShuttingDown || this.isRestarting) return;
|
||||
|
||||
console.warn(`Restarting QZPlayer: ${reason}`);
|
||||
this.isRestarting = true;
|
||||
this.emit('restart', { reason });
|
||||
|
||||
this.clearConnectTimer();
|
||||
this.clearRestartTimer();
|
||||
this.destroySocket();
|
||||
this.killProcess();
|
||||
|
||||
this.restartTimer = setTimeout(() => {
|
||||
this.restartTimer = null;
|
||||
this.isRestarting = false;
|
||||
if (!this.isShuttingDown) {
|
||||
this.start();
|
||||
}
|
||||
}, this.restartDelay);
|
||||
}
|
||||
|
||||
private handleData(data: Buffer): void {
|
||||
const raw = data.toString();
|
||||
|
||||
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.trackPlayerState(json);
|
||||
this.emit('message', json);
|
||||
|
||||
// Handle specific events
|
||||
if (json.event) {
|
||||
this.emit('event', json);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
console.error('Failed to parse QZPlayer message:', msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async send(command: any[]) {
|
||||
if (!this.socket || this.socket.destroyed) {
|
||||
async send(command: any[]): Promise<void> {
|
||||
this.trackOutgoingCommand(command);
|
||||
|
||||
if (!this.socket || this.socket.destroyed || !this.isConnected) {
|
||||
console.warn('QZPlayer socket not connected');
|
||||
this.queueCommand(command);
|
||||
if (!this.process && !this.isRestarting && !this.isShuttingDown) {
|
||||
this.start();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendNow(command);
|
||||
}
|
||||
|
||||
private sendNow(command: any[]): void {
|
||||
if (!this.socket || this.socket.destroyed || !this.isConnected) {
|
||||
this.queueCommand(command);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = JSON.stringify({ command });
|
||||
console.log('[QZPlayer TX]', payload); // User requested raw communication
|
||||
this.socket.write(payload + '\n');
|
||||
console.log('[QZPlayer TX]', payload);
|
||||
|
||||
try {
|
||||
this.socket.write(payload + '\n');
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
this.queueCommand(command);
|
||||
this.restart(`socket write failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience methods
|
||||
async load(url: string) {
|
||||
async load(url: string): Promise<void> {
|
||||
return this.send(['loadfile', url]);
|
||||
}
|
||||
|
||||
async play() {
|
||||
async play(): Promise<void> {
|
||||
return this.send(['set_property', 'pause', false]);
|
||||
}
|
||||
|
||||
async pause() {
|
||||
async pause(): Promise<void> {
|
||||
return this.send(['set_property', 'pause', true]);
|
||||
}
|
||||
|
||||
async togglePause() {
|
||||
async togglePause(): Promise<void> {
|
||||
return this.send(['cycle', 'pause']);
|
||||
}
|
||||
|
||||
async stop() {
|
||||
async stop(): Promise<void> {
|
||||
return this.send(['stop']);
|
||||
}
|
||||
|
||||
async setVolume(vol: number) {
|
||||
async setVolume(vol: number): Promise<void> {
|
||||
return this.send(['set_property', 'volume', vol]);
|
||||
}
|
||||
|
||||
async seek(seconds: number) {
|
||||
async seek(seconds: number): Promise<void> {
|
||||
return this.send(['seek', seconds, 'absolute']);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.process) {
|
||||
console.log('Killing QZPlayer process...');
|
||||
this.process.kill();
|
||||
this.process = null;
|
||||
}
|
||||
if (this.socket) {
|
||||
this.socket.destroy();
|
||||
destroy(): void {
|
||||
this.isShuttingDown = true;
|
||||
this.isRestarting = false;
|
||||
this.clearConnectTimer();
|
||||
this.clearRestartTimer();
|
||||
this.destroySocket();
|
||||
this.killProcess();
|
||||
}
|
||||
|
||||
private destroySocket(socket = this.socket): void {
|
||||
if (!socket) return;
|
||||
|
||||
if (this.socket === socket) {
|
||||
this.socket = null;
|
||||
this.isConnected = false;
|
||||
}
|
||||
|
||||
socket.removeAllListeners('data');
|
||||
socket.removeAllListeners('error');
|
||||
socket.removeAllListeners('close');
|
||||
socket.destroy();
|
||||
}
|
||||
|
||||
private killProcess(): void {
|
||||
const child = this.process;
|
||||
this.process = null;
|
||||
if (!child) return;
|
||||
|
||||
console.log('Killing QZPlayer process...');
|
||||
child.removeAllListeners('error');
|
||||
child.removeAllListeners('exit');
|
||||
if (!child.killed) {
|
||||
child.kill('SIGKILL');
|
||||
}
|
||||
}
|
||||
|
||||
private clearConnectTimer(): void {
|
||||
if (this.connectTimer) {
|
||||
clearTimeout(this.connectTimer);
|
||||
this.connectTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private clearRestartTimer(): void {
|
||||
if (this.restartTimer) {
|
||||
clearTimeout(this.restartTimer);
|
||||
this.restartTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private queueCommand(command: any[]): void {
|
||||
if (command[0] === 'observe_property') return;
|
||||
this.pendingCommands.push(command);
|
||||
if (this.pendingCommands.length > this.maxPendingCommands) {
|
||||
this.pendingCommands.shift();
|
||||
}
|
||||
}
|
||||
|
||||
private flushPendingCommands(): void {
|
||||
const commands = this.pendingCommands.splice(0);
|
||||
for (const command of commands) {
|
||||
this.sendNow(command);
|
||||
}
|
||||
}
|
||||
|
||||
private trackPlayerState(json: any): void {
|
||||
if (json?.event !== 'property-change') return;
|
||||
|
||||
if (json.name === 'pause') {
|
||||
this.desiredPause = Boolean(json.data);
|
||||
} else if (json.name === 'time-pos' && typeof json.data === 'number') {
|
||||
this.lastKnownTime = json.data;
|
||||
}
|
||||
}
|
||||
|
||||
private trackOutgoingCommand(command: any[]): void {
|
||||
const [name, ...args] = command;
|
||||
|
||||
if (name === 'loadfile' && typeof args[0] === 'string') {
|
||||
this.currentUrl = args[0];
|
||||
this.lastKnownTime = 0;
|
||||
} else if (name === 'seek' && typeof args[0] === 'number') {
|
||||
this.lastKnownTime = args[0];
|
||||
} else if (name === 'stop') {
|
||||
this.currentUrl = null;
|
||||
this.lastKnownTime = 0;
|
||||
this.desiredPause = true;
|
||||
} else if (name === 'cycle' && args[0] === 'pause') {
|
||||
this.desiredPause = !this.desiredPause;
|
||||
} else if (name === 'set_property') {
|
||||
if (args[0] === 'pause') {
|
||||
this.desiredPause = Boolean(args[1]);
|
||||
} else if (args[0] === 'volume' && typeof args[1] === 'number') {
|
||||
this.volume = args[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private emitError(err: Error): void {
|
||||
if (this.listenerCount('error') > 0) {
|
||||
this.emit('error', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user