diff --git a/dist-electron/main.js b/dist-electron/main.js index 0fa7604..d64fe58 100644 --- a/dist-electron/main.js +++ b/dist-electron/main.js @@ -1,14 +1,16 @@ var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); -import { ipcMain, BrowserWindow, app, Menu } from "electron"; +import { app, ipcMain, BrowserWindow, Menu } from "electron"; import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; import path$1 from "node:path"; +import fs$1 from "node:fs"; import { spawn } from "child_process"; import { Socket } from "net"; import { EventEmitter } from "events"; import path from "path"; +import fs from "fs"; class MpvController extends EventEmitter { constructor(ipcPath) { super(); @@ -142,6 +144,50 @@ class MpvController extends EventEmitter { } } } +const require$1 = createRequire(import.meta.url); +class PluginSystem { + constructor(pluginId) { + __publicField(this, "pluginId"); + __publicField(this, "plugin", null); + this.pluginId = pluginId; + this.loadPlugin(); + } + loadPlugin() { + try { + const pluginPath = path.join( + app.getPath("userData"), + "plugins", + this.pluginId, + "index.js" + ); + if (!fs.existsSync(pluginPath)) { + throw new Error(`Plugin ${this.pluginId} not found`); + } + delete require$1.cache[require$1.resolve(pluginPath)]; + this.plugin = require$1(pluginPath); + } catch (e) { + console.error(`[PluginSystem] load failed:`, e); + this.plugin = null; + } + } + async getUrl(id, quality) { + var _a; + if (!((_a = this.plugin) == null ? void 0 : _a.getUrl)) { + return { + success: false, + error: "getUrl not implemented" + }; + } + try { + return await this.plugin.getUrl(id, quality); + } catch (e) { + return { + success: false, + error: e.message || "plugin error" + }; + } + } +} createRequire(import.meta.url); const __dirname$1 = path$1.dirname(fileURLToPath(import.meta.url)); process.env.APP_ROOT = path$1.join(__dirname$1, ".."); @@ -173,6 +219,7 @@ function createWindow() { } else { win.loadFile(path$1.join(RENDERER_DIST, "index.html")); } + win.webContents.openDevTools(); registerZoomShortcuts(win); } ipcMain.on("window-minimize", (event) => { @@ -192,8 +239,21 @@ ipcMain.handle("mpv-play", () => mpv == null ? void 0 : mpv.play()); ipcMain.handle("mpv-pause", () => mpv == null ? void 0 : mpv.pause()); ipcMain.handle("mpv-toggle-pause", () => mpv == null ? void 0 : mpv.togglePause()); ipcMain.handle("mpv-stop", () => mpv == null ? void 0 : mpv.stop()); -ipcMain.handle("mpv-set-volume", (e, vol) => mpv == null ? void 0 : mpv.setVolume(vol)); +ipcMain.handle("mpv-set-volume", (_, vol) => mpv == null ? void 0 : mpv.setVolume(vol)); ipcMain.handle("mpv-seek", (_, time) => mpv == null ? void 0 : mpv.seek(time)); +ipcMain.handle( + "plugin:call", + async (_evenv, pluginId, method, args) => { + const plugin = new PluginSystem(pluginId); + if (typeof plugin[method] !== "function") { + return { + success: false, + error: `Method ${method} not found` + }; + } + return await plugin[method](...args); + } +); app.on("window-all-closed", () => { if (process.platform !== "darwin") { app.quit(); @@ -231,6 +291,29 @@ app.on("activate", () => { } }); app.whenReady().then(() => { + const pluginsPath = path$1.join(app.getPath("userData"), "plugins"); + if (!fs$1.existsSync(pluginsPath)) { + fs$1.mkdirSync(pluginsPath, { recursive: true }); + } + const wyPluginPath = path$1.join(pluginsPath, "wy"); + const wyPluginIndex = path$1.join(wyPluginPath, "index.js"); + if (!fs$1.existsSync(wyPluginIndex)) { + if (!fs$1.existsSync(wyPluginPath)) fs$1.mkdirSync(wyPluginPath, { recursive: true }); + fs$1.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(); mpv = new MpvController(); diff --git a/dist-electron/preload.mjs b/dist-electron/preload.mjs index 68159f0..747815e 100644 --- a/dist-electron/preload.mjs +++ b/dist-electron/preload.mjs @@ -16,5 +16,9 @@ electron.contextBridge.exposeInMainWorld("electronAPI", { setVolume: (vol) => electron.ipcRenderer.invoke("mpv-set-volume", vol), seek: (time) => electron.ipcRenderer.invoke("mpv-seek", time), onEvent: (callback) => electron.ipcRenderer.on("mpv-event", callback) + }, + // Plugin System + plugin: { + call: (pluginId, method, args) => electron.ipcRenderer.invoke("plugin:call", pluginId, method, args) } }); diff --git a/electron/main.ts b/electron/main.ts index 3b43874..5957be2 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -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() diff --git a/electron/mpvController.ts b/electron/mpvController.ts new file mode 100644 index 0000000..ef28a1a --- /dev/null +++ b/electron/mpvController.ts @@ -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; + } + } +} diff --git a/electron/preload.ts b/electron/preload.ts index 6756d68..cdea379 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -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) } }) \ No newline at end of file diff --git a/index.html b/index.html index dde16aa..ba7d41f 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,6 @@
- +