diff --git a/dist-electron/main.js b/dist-electron/main.js index fc9c43d..0fa7604 100644 --- a/dist-electron/main.js +++ b/dist-electron/main.js @@ -1,27 +1,168 @@ +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 { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; -import path from "node:path"; +import path$1 from "node:path"; +import { spawn } from "child_process"; +import { Socket } from "net"; +import { EventEmitter } from "events"; +import path from "path"; +class MpvController extends EventEmitter { + constructor(ipcPath) { + super(); + __publicField(this, "process", null); + __publicField(this, "socket", null); + __publicField(this, "ipcPath"); + __publicField(this, "messageBuffer", ""); + this.ipcPath = ipcPath || this.getIpcPath(); + } + getIpcPath() { + if (process.platform === "win32") { + return "\\\\.\\pipe\\qzmusic_mpv_socket"; + } + return "/tmp/qzmusic_mpv_socket"; + } + getMpvPath() { + const appRoot = process.env.APP_ROOT || process.cwd(); + if (process.platform === "win32") { + return path.join(appRoot, "core", "mpv.exe"); + } + 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) => { + var _a; + console.log(`MPV exited with code ${code} and signal ${signal}`); + this.emit("exit", { code, signal }); + (_a = this.socket) == null ? void 0 : _a.destroy(); + }); + this.tryConnect(); + } + 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) => { + var _a; + (_a = this.socket) == null ? void 0 : _a.destroy(); + this.tryConnect(retries - 1); + }); + this.socket.connect(this.ipcPath); + }, 500); + } + handleData(data) { + const raw = data.toString(); + this.messageBuffer += raw; + const messages = this.messageBuffer.split("\n"); + this.messageBuffer = messages.pop() || ""; + for (const msg of messages) { + if (!msg.trim()) continue; + console.log("[IPC]", msg); + try { + const json = JSON.parse(msg); + this.emit("message", json); + if (json.event) { + this.emit("event", json); + } + } catch (e) { + console.error("Failed to parse MPV message:", msg); + } + } + } + async send(command) { + if (!this.socket || this.socket.destroyed) { + console.warn("MPV socket not connected"); + return; + } + const payload = JSON.stringify({ command }); + console.log("[MPV TX]", payload); + this.socket.write(payload + "\n"); + } + // Convenience methods + async load(url) { + 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) { + return this.send(["set_property", "volume", vol]); + } + async seek(seconds) { + 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; + } + } +} createRequire(import.meta.url); -const __dirname$1 = path.dirname(fileURLToPath(import.meta.url)); -process.env.APP_ROOT = path.join(__dirname$1, ".."); +const __dirname$1 = path$1.dirname(fileURLToPath(import.meta.url)); +process.env.APP_ROOT = path$1.join(__dirname$1, ".."); const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"]; -const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron"); -const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist"); -process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, "public") : RENDERER_DIST; +const MAIN_DIST = path$1.join(process.env.APP_ROOT, "dist-electron"); +const RENDERER_DIST = path$1.join(process.env.APP_ROOT, "dist"); +process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path$1.join(process.env.APP_ROOT, "public") : RENDERER_DIST; let win; +let mpv; function createWindow() { win = new BrowserWindow({ frame: false, minWidth: 950, - minHeight: 700, + minHeight: 800, width: 1e3, height: 800, - icon: path.join(process.env.VITE_PUBLIC, "electron-vite.svg"), + icon: path$1.join(process.env.VITE_PUBLIC, "electron-vite.svg"), webPreferences: { - preload: path.join(__dirname$1, "preload.mjs"), - nodeIntegration: false, - contextIsolation: true + preload: path$1.join(__dirname$1, "preload.mjs") + // nodeIntegration: false, + // contextIsolation: true, } }); win.webContents.on("did-finish-load", () => { @@ -30,7 +171,7 @@ function createWindow() { if (VITE_DEV_SERVER_URL) { win.loadURL(VITE_DEV_SERVER_URL); } else { - win.loadFile(path.join(RENDERER_DIST, "index.html")); + win.loadFile(path$1.join(RENDERER_DIST, "index.html")); } registerZoomShortcuts(win); } @@ -41,12 +182,29 @@ ipcMain.on("window-minimize", (event) => { ipcMain.on("window-maximize", () => (win == null ? void 0 : win.isMaximized()) ? win.unmaximize() : win == null ? void 0 : win.maximize()); ipcMain.on("window-close", () => win == null ? void 0 : win.close()); ipcMain.handle("window-is-maximized", () => (win == null ? void 0 : win.isMaximized()) || false); +ipcMain.handle("mpv-command", async (_, command) => { + if (mpv) { + mpv.send(command); + } +}); +ipcMain.handle("mpv-load", (_, url) => mpv == null ? void 0 : mpv.load(url)); +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-seek", (_, time) => mpv == null ? void 0 : mpv.seek(time)); app.on("window-all-closed", () => { if (process.platform !== "darwin") { app.quit(); win = null; } }); +app.on("will-quit", () => { + if (mpv) { + mpv.destroy(); + } +}); function registerZoomShortcuts(win2) { win2.webContents.on("before-input-event", (event, input) => { if (input.control || input.meta) { @@ -75,6 +233,13 @@ app.on("activate", () => { app.whenReady().then(() => { Menu.setApplicationMenu(null); createWindow(); + mpv = new MpvController(); + mpv.start(); + mpv.on("event", (data) => { + if (win && !win.isDestroyed()) { + win.webContents.send("mpv-event", data); + } + }); }); export { MAIN_DIST, diff --git a/dist-electron/preload.mjs b/dist-electron/preload.mjs index 674120e..68159f0 100644 --- a/dist-electron/preload.mjs +++ b/dist-electron/preload.mjs @@ -5,5 +5,16 @@ electron.contextBridge.exposeInMainWorld("electronAPI", { minimizeWindow: () => electron.ipcRenderer.send("window-minimize"), maximizeWindow: () => electron.ipcRenderer.send("window-maximize"), closeWindow: () => electron.ipcRenderer.send("window-close"), - isMaximized: () => electron.ipcRenderer.invoke("window-is-maximized") + isMaximized: () => electron.ipcRenderer.invoke("window-is-maximized"), + // MPV Control + mpv: { + load: (url) => electron.ipcRenderer.invoke("mpv-load", url), + play: () => electron.ipcRenderer.invoke("mpv-play"), + pause: () => electron.ipcRenderer.invoke("mpv-pause"), + togglePause: () => electron.ipcRenderer.invoke("mpv-toggle-pause"), + stop: () => electron.ipcRenderer.invoke("mpv-stop"), + 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) + } }); diff --git a/electron/main.ts b/electron/main.ts index 17b0747..3b43874 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -2,6 +2,7 @@ import { app, BrowserWindow, Menu, ipcMain } from 'electron' import { createRequire } from 'node:module' import { fileURLToPath } from 'node:url' import path from 'node:path' +import { MpvController } from './mpvController' // @ts-ignore const require = createRequire(import.meta.url) @@ -16,6 +17,7 @@ export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist') process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST let win: BrowserWindow | null +let mpv: MpvController | null // === Electron 窗口逻辑 === @@ -23,14 +25,14 @@ function createWindow() { win = new BrowserWindow({ frame: false, minWidth: 950, - minHeight: 700, + minHeight: 800, width: 1000, height: 800, icon: path.join(process.env.VITE_PUBLIC, 'electron-vite.svg'), webPreferences: { preload: path.join(__dirname, 'preload.mjs'), - nodeIntegration: false, - contextIsolation: true, + // nodeIntegration: false, + // contextIsolation: true, }, }) @@ -54,6 +56,23 @@ ipcMain.on('window-maximize', () => win?.isMaximized() ? win.unmaximize() : win? ipcMain.on('window-close', () => win?.close()) ipcMain.handle('window-is-maximized', () => win?.isMaximized() || false) +// --- MPV IPC Handlers --- +ipcMain.handle('mpv-command', async (_, command: any[]) => { + if (mpv) { + mpv.send(command) + } +}) + +// Quick Helpers +ipcMain.handle('mpv-load', (_, url) => mpv?.load(url)) +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-seek', (_, time) => mpv?.seek(time)) + + app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() @@ -61,6 +80,12 @@ app.on('window-all-closed', () => { } }) +app.on('will-quit', () => { + if (mpv) { + mpv.destroy() + } +}) + function registerZoomShortcuts(win: BrowserWindow) { win.webContents.on('before-input-event', (event, input) => { if (input.control || input.meta) { @@ -92,4 +117,15 @@ app.on('activate', () => { app.whenReady().then(() => { Menu.setApplicationMenu(null) createWindow() + + // Start MPV + mpv = new MpvController() + mpv.start() + + mpv.on('event', (data) => { + // Forward MPV events to Render Process + if (win && !win.isDestroyed()) { + win.webContents.send('mpv-event', data) + } + }) }) diff --git a/electron/preload.ts b/electron/preload.ts index 87d627e..6756d68 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -6,4 +6,16 @@ contextBridge.exposeInMainWorld('electronAPI', { maximizeWindow: () => ipcRenderer.send('window-maximize'), closeWindow: () => ipcRenderer.send('window-close'), isMaximized: () => ipcRenderer.invoke('window-is-maximized'), + + // MPV Control + mpv: { + load: (url: string) => ipcRenderer.invoke('mpv-load', url), + play: () => ipcRenderer.invoke('mpv-play'), + pause: () => ipcRenderer.invoke('mpv-pause'), + togglePause: () => ipcRenderer.invoke('mpv-toggle-pause'), + stop: () => ipcRenderer.invoke('mpv-stop'), + 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) + } }) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 58bf341..0c19ab1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "element-plus": "^2.13.0", "jss": "^10.10.0", "jss-preset-default": "^10.10.0", - "node-mpv": "^1.5.0", "pinia": "^3.0.4", "tdesign-vue-next": "^1.17.7", "vue": "^3.4.21", @@ -2670,12 +2669,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmmirror.com/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "license": "MIT" - }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/assert-plus/-/assert-plus-1.0.0.tgz", @@ -2844,12 +2837,6 @@ "node": ">=8" } }, - "node_modules/browser-fingerprint": { - "version": "0.0.1", - "resolved": "https://registry.npmmirror.com/browser-fingerprint/-/browser-fingerprint-0.0.1.tgz", - "integrity": "sha512-b8SXP7yOlzLUJXF8WUvIjmbJzkJC0X6OHe7J9a/SHqEBC7a9Eglag6AANSTJz82h5U582kuxm/5TPudnD68EPA==", - "license": "MIT" - }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", @@ -3317,13 +3304,6 @@ "url": "https://github.com/sponsors/mesqueeb" } }, - "node_modules/core-js": { - "version": "1.2.7", - "resolved": "https://registry.npmmirror.com/core-js/-/core-js-1.2.7.tgz", - "integrity": "sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "license": "MIT" - }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz", @@ -3402,18 +3382,6 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, - "node_modules/cuid": { - "version": "1.3.8", - "resolved": "https://registry.npmmirror.com/cuid/-/cuid-1.3.8.tgz", - "integrity": "sha512-MoL67ZZuBetDMxzrZtO+Iq1ATajFACQCP52QRinBgd3yTjYdv54mJO8ibUrh06fojKCoX5P2i7KkEatm4VTIOQ==", - "deprecated": "Cuid and other k-sortable and non-cryptographic ids (Ulid, ObjectId, KSUID, all UUIDs) are all insecure. Use @paralleldrive/cuid2 instead.", - "license": "MIT", - "dependencies": { - "browser-fingerprint": "0.0.1", - "core-js": "^1.1.1", - "node-fingerprint": "0.0.2" - } - }, "node_modules/dayjs": { "version": "1.11.19", "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", @@ -5503,22 +5471,6 @@ "license": "MIT", "optional": true }, - "node_modules/node-fingerprint": { - "version": "0.0.2", - "resolved": "https://registry.npmmirror.com/node-fingerprint/-/node-fingerprint-0.0.2.tgz", - "integrity": "sha512-vPFfTD5EBJieQ4SI3v61fWxlV1kav3m9Dbejd6CjWhOJn8s+XMxpOOosCNAyIrUQ/jJOlPndfrZ0lSw4+RgwcA==", - "license": "MIT" - }, - "node_modules/node-mpv": { - "version": "1.5.0", - "resolved": "https://registry.npmmirror.com/node-mpv/-/node-mpv-1.5.0.tgz", - "integrity": "sha512-kvLo+PcWHZ/Sg7t9XeFDi5KJrNOL9XJOEljCEh5wBNOHiE6Wa/txwIsYWKmNaIFuncbEhgjnoOavE4T5YBNV8Q==", - "dependencies": { - "cuid": "^1.3.8", - "lodash": ">= 4.0.0", - "promise": "^7.1.1" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", @@ -5765,15 +5717,6 @@ "node": ">=0.4.0" } }, - "node_modules/promise": { - "version": "7.3.1", - "resolved": "https://registry.npmmirror.com/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "license": "MIT", - "dependencies": { - "asap": "~2.0.3" - } - }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/promise-retry/-/promise-retry-2.0.1.tgz", diff --git a/package.json b/package.json index 0c35513..52a44a4 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "element-plus": "^2.13.0", "jss": "^10.10.0", "jss-preset-default": "^10.10.0", - "node-mpv": "^1.5.0", "pinia": "^3.0.4", "tdesign-vue-next": "^1.17.7", "vue": "^3.4.21", diff --git a/src/main.ts b/src/main.ts index fdd6220..b180074 100644 --- a/src/main.ts +++ b/src/main.ts @@ -20,6 +20,16 @@ const router = createRouter({ path: '/local', name: 'Local', component: () => import('./views/LocalMusic.vue') + }, + { + path: '/liked', + name: 'Liked', + component: () => import('./views/Playlist.vue') + }, + { + path: '/recent', + name: 'Recent', + component: () => import('./views/Playlist.vue') } ] }) @@ -28,4 +38,29 @@ const app = createApp(App) app.use(pinia) app.use(router) app.use(TDesign) -app.mount('#app') \ No newline at end of file +app.mount('#app') + +// --- TEST: Auto Play Specific Song --- +import { usePlayerStore, PlayMode } from './stores/player' +import type { Song } from './types/song' + +const playerStore = usePlayerStore() + +// Set Single Mode +playerStore.playMode = PlayMode.Single; + +const testSong: Song = { + id: '9999', + name: 'Test FLAC', + artist: 'Netease', + picUrl: 'http://p1.music.126.net/btYBbFLd5mf9w0lDpfNs6w==/109951171506809884.jpg?param=130y130', + url: 'http://m801.music.126.net/20260202213928/189743bba596f8fd999bc44fd51d11fd/jdymusic/obj/wo3DlMOGwrbDjj7DisKw/61393856655/24be/6a68/77e4/fb898a5378682427bf7a4fb55640e610.flac', + duration: '03:30', + source: 'netease', + type: 'Remote' +}; + +// Auto Play +playerStore.playSong(testSong).then(() => { + playerStore.setPlaylist([testSong]); +}); \ No newline at end of file diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index bcb3eb0..5de43b5 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -3,6 +3,16 @@ export interface IElectronAPI { maximizeWindow: () => void; closeWindow: () => void; isMaximized: () => Promise; + mpv: { + load: (url: string) => Promise; + play: () => Promise; + pause: () => Promise; + togglePause: () => Promise; + stop: () => Promise; + setVolume: (vol: number) => Promise; + seek: (time: number) => Promise; + onEvent: (callback: (event: any, data: any) => void) => void; + } } declare global {