mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 12:55:16 -07:00
feat: add mark-watched action, background app reuse, and N+1 compat
- Add `--mark-watched` CLI flag + mpv session binding; marks video watched, shows OSD, advances playlist - Launcher detects running background app via `--app-ping` and borrows it instead of owning its lifecycle - Preserve N+1 highlighting for existing configs with `knownWords.highlightEnabled` set - Fix `resolveConfiguredShortcuts` to respect explicit `null` overrides (disabling defaults) - Split session-help modal into focused modules (colors, render, sections, tabs)
This commit is contained in:
@@ -655,6 +655,48 @@ test('startOverlay captures app stdout and stderr into app log', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('startOverlay borrows an already-running background app instead of owning its lifecycle', async () => {
|
||||
const { dir, socketPath } = createTempSocketPath();
|
||||
const appPath = path.join(dir, 'fake-subminer.sh');
|
||||
const appInvocationsPath = path.join(dir, 'app-invocations.log');
|
||||
fs.writeFileSync(
|
||||
appPath,
|
||||
[
|
||||
'#!/bin/sh',
|
||||
`printf '%s\\n' "$@" >> ${JSON.stringify(appInvocationsPath)}`,
|
||||
'if [ "$1" = "--app-ping" ]; then exit 0; fi',
|
||||
'exit 0',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
fs.chmodSync(appPath, 0o755);
|
||||
fs.writeFileSync(socketPath, '');
|
||||
const originalCreateConnection = net.createConnection;
|
||||
try {
|
||||
net.createConnection = (() => {
|
||||
const socket = new EventEmitter() as net.Socket;
|
||||
socket.destroy = (() => socket) as net.Socket['destroy'];
|
||||
socket.setTimeout = (() => socket) as net.Socket['setTimeout'];
|
||||
setTimeout(() => socket.emit('connect'), 10);
|
||||
return socket;
|
||||
}) as typeof net.createConnection;
|
||||
|
||||
await startOverlay(appPath, makeArgs(), socketPath);
|
||||
|
||||
const invocationText = fs.readFileSync(appInvocationsPath, 'utf8');
|
||||
assert.match(invocationText, /--app-ping/);
|
||||
assert.match(invocationText, /--start/);
|
||||
assert.equal(state.overlayManagedByLauncher, false);
|
||||
assert.equal(state.appPath, '');
|
||||
} finally {
|
||||
net.createConnection = originalCreateConnection;
|
||||
state.overlayProc = null;
|
||||
state.overlayManagedByLauncher = false;
|
||||
state.appPath = '';
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('cleanupPlaybackSession stops launcher-managed overlay app and mpv-owned children', async () => {
|
||||
const { dir } = createTempSocketPath();
|
||||
const appPath = path.join(dir, 'fake-subminer.sh');
|
||||
|
||||
+25
-1
@@ -1004,6 +1004,7 @@ export async function startOverlay(
|
||||
): Promise<void> {
|
||||
const backend = detectBackend(args.backend);
|
||||
log('info', args.logLevel, `Starting SubMiner overlay (backend: ${backend})...`);
|
||||
const appAlreadyRunning = isAppAlreadyRunning(appPath, args.logLevel);
|
||||
|
||||
const overlayArgs = ['--start', '--backend', backend, '--socket', socketPath, ...extraAppArgs];
|
||||
if (args.logLevel !== 'info') overlayArgs.push('--log-level', args.logLevel);
|
||||
@@ -1015,7 +1016,16 @@ export async function startOverlay(
|
||||
env: buildAppEnv(process.env, target.env),
|
||||
});
|
||||
attachAppProcessLogging(state.overlayProc);
|
||||
markOverlayManagedByLauncher(appPath);
|
||||
if (appAlreadyRunning) {
|
||||
log(
|
||||
'debug',
|
||||
args.logLevel,
|
||||
'SubMiner app is already running; launcher will not stop it after playback',
|
||||
);
|
||||
clearOverlayManagedByLauncher();
|
||||
} else {
|
||||
markOverlayManagedByLauncher(appPath);
|
||||
}
|
||||
|
||||
const [socketReady] = await Promise.all([
|
||||
waitForUnixSocketReady(socketPath, OVERLAY_START_SOCKET_READY_TIMEOUT_MS),
|
||||
@@ -1042,6 +1052,20 @@ export function markOverlayManagedByLauncher(appPath?: string): void {
|
||||
state.overlayManagedByLauncher = true;
|
||||
}
|
||||
|
||||
function clearOverlayManagedByLauncher(): void {
|
||||
state.appPath = '';
|
||||
state.overlayManagedByLauncher = false;
|
||||
}
|
||||
|
||||
function isAppAlreadyRunning(appPath: string, logLevel: LogLevel): boolean {
|
||||
const result = runSyncAppCommand(appPath, ['--app-ping'], false);
|
||||
if (result.error) {
|
||||
log('debug', logLevel, `App ping failed before overlay start: ${result.error.message}`);
|
||||
return false;
|
||||
}
|
||||
return result.status === 0;
|
||||
}
|
||||
|
||||
export function openUrlInDefaultBrowser(url: string, logLevel: LogLevel): void {
|
||||
const target =
|
||||
process.platform === 'darwin'
|
||||
|
||||
@@ -133,6 +133,9 @@ if (entry.argv.includes('--start')) {
|
||||
if (entry.argv.includes('--stop')) {
|
||||
fs.appendFileSync(stopPath, JSON.stringify(entry) + '\\n');
|
||||
}
|
||||
if (entry.argv.includes('--app-ping')) {
|
||||
process.exit(process.env.SUBMINER_FAKE_APP_RUNNING === '1' ? 0 : 1);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
`,
|
||||
@@ -347,6 +350,49 @@ test(
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'launcher start-overlay borrows a running background app and does not stop it after mpv exits',
|
||||
{ timeout: LONG_SMOKE_TEST_TIMEOUT_MS },
|
||||
async () => {
|
||||
await withSmokeCase('overlay-borrow-background', async (smokeCase) => {
|
||||
const env = {
|
||||
...makeTestEnv(smokeCase),
|
||||
SUBMINER_FAKE_APP_RUNNING: '1',
|
||||
};
|
||||
const result = runLauncher(
|
||||
smokeCase,
|
||||
['--backend', 'x11', '--start-overlay', smokeCase.videoPath],
|
||||
env,
|
||||
'overlay-borrow-background',
|
||||
);
|
||||
|
||||
const appLogPath = path.join(smokeCase.artifactsDir, 'fake-app.log');
|
||||
const appStartPath = path.join(smokeCase.artifactsDir, 'fake-app-start.log');
|
||||
const appStopPath = path.join(smokeCase.artifactsDir, 'fake-app-stop.log');
|
||||
await waitForJsonLines(appStartPath, 1);
|
||||
|
||||
const appEntries = readJsonLines(appLogPath);
|
||||
const appStartEntries = readJsonLines(appStartPath);
|
||||
const appStopEntries = readJsonLines(appStopPath);
|
||||
const mpvEntries = readJsonLines(path.join(smokeCase.artifactsDir, 'fake-mpv.log'));
|
||||
const mpvError = mpvEntries.find(
|
||||
(entry): entry is { error: string } => typeof entry.error === 'string',
|
||||
)?.error;
|
||||
const unixSocketDenied =
|
||||
typeof mpvError === 'string' && /eperm|operation not permitted/i.test(mpvError);
|
||||
|
||||
assert.equal(result.status, unixSocketDenied ? 3 : 0);
|
||||
assert.ok(
|
||||
appEntries.some(
|
||||
(entry) => Array.isArray(entry.argv) && (entry.argv as string[]).includes('--app-ping'),
|
||||
),
|
||||
);
|
||||
assert.equal(appStartEntries.length, 1);
|
||||
assert.equal(appStopEntries.length, 0);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'launcher starts mpv paused when plugin auto-start visible overlay gate is enabled',
|
||||
{ timeout: LONG_SMOKE_TEST_TIMEOUT_MS },
|
||||
|
||||
Reference in New Issue
Block a user