Fix Windows mpv logging and add log export (#88)

This commit is contained in:
2026-05-26 00:31:38 -07:00
committed by GitHub
parent 43ebc7d371
commit 11c196821d
150 changed files with 2748 additions and 582 deletions
+30 -2
View File
@@ -6,6 +6,7 @@ import type { LauncherCommandContext } from './context.js';
import { runConfigCommand } from './config-command.js';
import { runDictionaryCommand } from './dictionary-command.js';
import { runDoctorCommand } from './doctor-command.js';
import { runLogsCommand } from './logs-command.js';
import { runMpvPreAppCommand } from './mpv-command.js';
import { runAppPassthroughCommand } from './app-command.js';
import { runStatsCommand } from './stats-command.js';
@@ -169,6 +170,33 @@ test('doctor command forwards refresh-known-words to app binary', () => {
assert.deepEqual(forwarded, [['--refresh-known-words']]);
});
test('logs command exports logs and writes archive path', () => {
const writes: string[] = [];
const context = createContext();
context.args.logsExport = true;
context.processAdapter = {
...context.processAdapter,
writeStdout: (text) => writes.push(text),
};
const handled = runLogsCommand(context, {
exportLogsArchive: () => ({
zipPath: '/tmp/subminer-logs.zip',
exportedFiles: ['/tmp/app.log'],
mode: 'current-day',
}),
});
assert.equal(handled, true);
assert.deepEqual(writes, ['/tmp/subminer-logs.zip\n']);
});
test('logs command ignores unrelated launcher commands', () => {
const context = createContext();
assert.equal(runLogsCommand(context), false);
});
test('app command starts default macOS background app detached from launcher', () => {
const context = createContext();
context.args.appPassthrough = true;
@@ -185,7 +213,7 @@ test('app command starts default macOS background app detached from launcher', (
});
assert.equal(handled, true);
assert.deepEqual(calls, ['detached:/tmp/subminer.app:info']);
assert.deepEqual(calls, ['detached:/tmp/subminer.app:warn']);
});
test('app command starts default Linux background app detached from launcher', () => {
@@ -204,7 +232,7 @@ test('app command starts default Linux background app detached from launcher', (
});
assert.equal(handled, true);
assert.deepEqual(calls, ['detached:/tmp/subminer.app:info']);
assert.deepEqual(calls, ['detached:/tmp/subminer.app:warn']);
});
test('app command keeps explicit passthrough args attached', () => {
+2 -1
View File
@@ -1,4 +1,5 @@
import { runAppCommandWithInherit } from '../mpv.js';
import { shouldForwardLogLevel } from '../types.js';
import type { LauncherCommandContext } from './context.js';
interface DictionaryCommandDeps {
@@ -35,7 +36,7 @@ export function runDictionaryCommand(
if (typeof args.dictionaryTarget === 'string' && args.dictionaryTarget.trim()) {
forwarded.push('--dictionary-target', args.dictionaryTarget);
}
if (args.logLevel !== 'info') {
if (shouldForwardLogLevel(args.logLevel)) {
forwarded.push('--log-level', args.logLevel);
}
+5 -4
View File
@@ -2,6 +2,7 @@ import { fail } from '../log.js';
import { runAppCommandWithInherit } from '../mpv.js';
import { commandExists } from '../util.js';
import { runJellyfinPlayMenu } from '../jellyfin.js';
import { shouldForwardLogLevel } from '../types.js';
import type { LauncherCommandContext } from './context.js';
export async function runJellyfinCommand(context: LauncherCommandContext): Promise<boolean> {
@@ -18,7 +19,7 @@ export async function runJellyfinCommand(context: LauncherCommandContext): Promi
if (args.jellyfin) {
const forwarded = ['--jellyfin'];
if (args.logLevel !== 'info') forwarded.push('--log-level', args.logLevel);
if (shouldForwardLogLevel(args.logLevel)) forwarded.push('--log-level', args.logLevel);
appendPasswordStore(forwarded);
runAppCommandWithInherit(appPath, forwarded);
return true;
@@ -42,7 +43,7 @@ export async function runJellyfinCommand(context: LauncherCommandContext): Promi
'--jellyfin-password',
password,
];
if (args.logLevel !== 'info') forwarded.push('--log-level', args.logLevel);
if (shouldForwardLogLevel(args.logLevel)) forwarded.push('--log-level', args.logLevel);
appendPasswordStore(forwarded);
runAppCommandWithInherit(appPath, forwarded);
return true;
@@ -50,7 +51,7 @@ export async function runJellyfinCommand(context: LauncherCommandContext): Promi
if (args.jellyfinLogout) {
const forwarded = ['--jellyfin-logout'];
if (args.logLevel !== 'info') forwarded.push('--log-level', args.logLevel);
if (shouldForwardLogLevel(args.logLevel)) forwarded.push('--log-level', args.logLevel);
appendPasswordStore(forwarded);
runAppCommandWithInherit(appPath, forwarded);
return true;
@@ -69,7 +70,7 @@ export async function runJellyfinCommand(context: LauncherCommandContext): Promi
if (args.jellyfinDiscovery) {
const forwarded = ['--background', '--jellyfin-remote-announce'];
if (args.logLevel !== 'info') forwarded.push('--log-level', args.logLevel);
if (shouldForwardLogLevel(args.logLevel)) forwarded.push('--log-level', args.logLevel);
appendPasswordStore(forwarded);
runAppCommandWithInherit(appPath, forwarded);
return true;
+24
View File
@@ -0,0 +1,24 @@
import { exportLogsArchiveForCurrentUser } from '../../src/main/runtime/log-export.js';
import type { ExportLogsResult } from '../../src/main/runtime/log-export.js';
import type { LauncherCommandContext } from './context.js';
interface LogsCommandDeps {
exportLogsArchive(): ExportLogsResult;
}
const defaultDeps: LogsCommandDeps = {
exportLogsArchive: () => exportLogsArchiveForCurrentUser(),
};
export function runLogsCommand(
context: LauncherCommandContext,
deps: LogsCommandDeps = defaultDeps,
): boolean {
if (!context.args.logsExport) {
return false;
}
const result = deps.exportLogsArchive();
context.processAdapter.writeStdout(`${result.zipPath}\n`);
return true;
}
@@ -36,6 +36,7 @@ function createContext(): LauncherCommandContext {
texthookerOpenBrowser: false,
useRofi: false,
logLevel: 'info',
logRotation: 7,
passwordStore: '',
target: 'https://www.youtube.com/watch?v=65Ovd7t8sNw',
targetKind: 'url',
@@ -55,6 +56,7 @@ function createContext(): LauncherCommandContext {
stats: false,
doctor: false,
doctorRefreshKnownWords: false,
logsExport: false,
version: false,
settings: false,
configPath: false,
@@ -321,6 +323,7 @@ test('plugin auto-start playback attaches a warm background app through the laun
test('plugin auto-start attach mode reuses launcher-resolved config dir for app control', async () => {
const context = createContext();
const originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
const originalAppData = process.env.APPDATA;
const xdgConfigHome = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-test-xdg-'));
const expectedConfigDir = path.join(xdgConfigHome, 'SubMiner');
fs.mkdirSync(expectedConfigDir, { recursive: true });
@@ -347,6 +350,7 @@ test('plugin auto-start attach mode reuses launcher-resolved config dir for app
try {
process.env.XDG_CONFIG_HOME = xdgConfigHome;
process.env.APPDATA = xdgConfigHome;
await runPlaybackCommandWithDeps(context, {
ensurePlaybackSetupReady: async () => {},
@@ -376,6 +380,11 @@ test('plugin auto-start attach mode reuses launcher-resolved config dir for app
} else {
process.env.XDG_CONFIG_HOME = originalXdgConfigHome;
}
if (originalAppData === undefined) {
delete process.env.APPDATA;
} else {
process.env.APPDATA = originalAppData;
}
fs.rmSync(xdgConfigHome, { recursive: true, force: true });
}
});
+2 -1
View File
@@ -4,6 +4,7 @@ import path from 'node:path';
import { runAppCommandAttached } from '../mpv.js';
import { nowMs } from '../time.js';
import { sleep } from '../util.js';
import { shouldForwardLogLevel } from '../types.js';
import type { LauncherCommandContext } from './context.js';
type StatsCommandResponse = {
@@ -156,7 +157,7 @@ export async function runStatsCommand(
if (args.statsCleanupLifetime) {
forwarded.push('--stats-cleanup-lifetime');
}
if (args.logLevel !== 'info') {
if (shouldForwardLogLevel(args.logLevel)) {
forwarded.push('--log-level', args.logLevel);
}
const attachedExitPromise = resolvedDeps.runAppCommandAttached(