// 离线 mpv IPC:只读写测试目录内的 JSON;禁止真实 socket 与启动播放器。 import net from 'node:net'; import childProcess from 'node:child_process'; import { EventEmitter } from 'node:events'; import { appendFileSync, readFileSync, writeFileSync } from 'node:fs'; import { syncBuiltinESMExports } from 'node:module'; import { join } from 'node:path'; childProcess.spawn = () => { throw new Error('离线测试禁止启动真实进程/播放器'); }; net.connect = (socket) => { const file = process.env.NETEASE_TEST_MPV_STATE; if (!file || socket !== join(process.env.NETEASE_MPV_DIR, 'mpv.sock')) { throw new Error('离线测试禁止连接真实 mpv socket'); } const client = new EventEmitter(); client.destroy = () => {}; client.end = () => {}; client.write = (line) => { const { command, request_id } = JSON.parse(line); appendFileSync(`${file}.calls`, JSON.stringify(command) + '\n'); const state = JSON.parse(readFileSync(file, 'utf8')); const [op, property, value] = command; let data, error = 'success'; const current = () => state.entries[state.pos]; if (state.fail && command.slice(0, state.fail.length).every((v, i) => v === state.fail[i])) { error = 'fixture rejected command'; } else if (op === 'get_property') { const props = { 'playlist-pos': state.pos, 'playlist-count': state.entries.length, playlist: state.entries, 'idle-active': !current(), pause: state.pause, 'time-pos': current() ? state.elapsed : null, duration: current() ? 180 : null, volume: 70, 'media-title': current()?.title || '', path: current()?.filename || '', 'loop-file': state.loopFile, 'loop-playlist': state.loopList, shuffle: state.shuffle, }; if (property in props) data = props[property]; else error = `unknown fixture property ${property}`; } else if (op === 'set_property') { if (property === 'playlist-pos') { if (!Number.isInteger(value) || value < 0 || value >= state.entries.length) error = 'invalid index'; else { state.pos = value; state.elapsed = 0; } } else { const key = { pause: 'pause', shuffle: 'shuffle', 'loop-file': 'loopFile', 'loop-playlist': 'loopList' }[property]; if (key) state[key] = value; else error = `unknown fixture property ${property}`; } } else if (op === 'loadlist') { const lines = readFileSync(property, 'utf8').trim().split('\n'); const entries = []; for (let i = 0; i < lines.length; i++) { if (lines[i].startsWith('#EXTINF:')) entries.push({ title: lines[i].slice(lines[i].indexOf(',') + 1), filename: lines[++i] }); } state.entries = value === 'append' ? [...state.entries, ...entries] : entries; if (value !== 'append') { state.pos = entries.length ? 0 : -1; state.elapsed = 0; } } else if (op === 'playlist-remove') { if (!Number.isInteger(property) || property < 0 || property >= state.entries.length) error = 'invalid index'; else { state.entries.splice(property, 1); if (property < state.pos) state.pos--; else if (property === state.pos && state.pos >= state.entries.length) state.pos = -1; } } else if (op === 'stop') { state.entries = []; state.pos = -1; } else if (op === 'playlist-clear') { // mpv 的 playlist-clear 自身会保留正在播放的条目;调用方必须先 stop。 state.entries = current() ? [current()] : []; state.pos = state.entries.length ? 0 : -1; } else error = `unknown fixture command ${op}`; writeFileSync(file, JSON.stringify(state)); queueMicrotask(() => client.emit('data', Buffer.from(JSON.stringify({ request_id, error, data }) + '\n'))); }; queueMicrotask(() => client.emit('connect')); return client; }; syncBuiltinESMExports();