mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-28 16:49:50 -07:00
Fix Windows mpv logging and add log export (#88)
This commit is contained in:
+123
-35
@@ -123,10 +123,16 @@ import { RuntimeOptionsManager } from './runtime-options';
|
||||
import { downloadToFile, isRemoteMediaPath, parseMediaInfo } from './jimaku/utils';
|
||||
import {
|
||||
createLogger,
|
||||
setLogFileToggles,
|
||||
setLogLevel,
|
||||
resolveDefaultLogFilePath,
|
||||
setLogRotation,
|
||||
type LogLevelSource,
|
||||
} from './logger';
|
||||
import {
|
||||
isLogFileEnabled,
|
||||
pruneLogDirectoryForPath,
|
||||
resolveDefaultLogFilePath,
|
||||
} from './shared/log-files';
|
||||
import { createFatalErrorReporter } from './main/fatal-error';
|
||||
import { createWindowTracker as createWindowTrackerCore } from './window-trackers';
|
||||
import {
|
||||
@@ -148,6 +154,7 @@ import {
|
||||
} from './cli/args';
|
||||
import { printHelp } from './cli/help';
|
||||
import { IPC_CHANNELS, type OverlayHostedModal } from './shared/ipc/contracts';
|
||||
import { buildMpvLoggingArgs } from './shared/mpv-logging-args';
|
||||
import { AnkiConnectClient } from './anki-connect';
|
||||
import {
|
||||
getStartupModeFlags,
|
||||
@@ -258,6 +265,7 @@ import {
|
||||
createMpvOsdRuntimeHandlers,
|
||||
createCycleSecondarySubModeRuntimeHandler,
|
||||
} from './main/runtime/domains/mpv';
|
||||
import { buildSubtitleTrackDiagnostics } from './main/runtime/mpv-track-diagnostics';
|
||||
import {
|
||||
createBuildCopyCurrentSubtitleMainDepsHandler,
|
||||
createBuildHandleMineSentenceDigitMainDepsHandler,
|
||||
@@ -430,6 +438,7 @@ import {
|
||||
shouldQuitOnMpvShutdownForTrayState,
|
||||
shouldQuitOnWindowAllClosedForTrayState,
|
||||
} from './main/runtime/startup-tray-policy';
|
||||
import { exportLogsArchive } from './main/runtime/log-export';
|
||||
import { createImmersionTrackerStartupHandler } from './main/runtime/immersion-startup';
|
||||
import { createBuildImmersionTrackerStartupMainDepsHandler } from './main/runtime/immersion-startup-main-deps';
|
||||
import {
|
||||
@@ -500,10 +509,7 @@ import { openRuntimeOptionsModal as openRuntimeOptionsModalRuntime } from './mai
|
||||
import { openJimakuModal as openJimakuModalRuntime } from './main/runtime/jimaku-open';
|
||||
import { openSubsyncManualModal as openSubsyncManualModalRuntime } from './main/runtime/subsync-open';
|
||||
import { openSessionHelpModal as openSessionHelpModalRuntime } from './main/runtime/session-help-open';
|
||||
import {
|
||||
openCharacterDictionaryManagerModal as openCharacterDictionaryManagerModalRuntime,
|
||||
openCharacterDictionaryModal as openCharacterDictionaryModalRuntime,
|
||||
} from './main/runtime/character-dictionary-open';
|
||||
import { openCharacterDictionaryManagerModal as openCharacterDictionaryManagerModalRuntime } from './main/runtime/character-dictionary-open';
|
||||
import { openControllerSelectModal as openControllerSelectModalRuntime } from './main/runtime/controller-select-open';
|
||||
import { openControllerDebugModal as openControllerDebugModalRuntime } from './main/runtime/controller-debug-open';
|
||||
import { createPlaylistBrowserIpcRuntime } from './main/runtime/playlist-browser-ipc';
|
||||
@@ -532,6 +538,7 @@ import {
|
||||
} from './main/runtime/character-dictionary-auto-sync';
|
||||
import { handleCharacterDictionaryAutoSyncComplete } from './main/runtime/character-dictionary-auto-sync-completion';
|
||||
import { notifyCharacterDictionaryAutoSyncStatus } from './main/runtime/character-dictionary-auto-sync-notifications';
|
||||
import { openCharacterDictionaryManagerWithConfigGate } from './main/runtime/character-dictionary-manager-gate';
|
||||
import { createCurrentMediaTokenizationGate } from './main/runtime/current-media-tokenization-gate';
|
||||
import { resolveCurrentSubtitleForRenderer } from './main/runtime/current-subtitle-snapshot';
|
||||
import { createJellyfinSubtitleCacheIo } from './main/runtime/jellyfin-subtitle-cache-io';
|
||||
@@ -641,7 +648,7 @@ if (process.platform === 'linux') {
|
||||
app.setName('SubMiner');
|
||||
|
||||
const DEFAULT_TEXTHOOKER_PORT = 5174;
|
||||
const DEFAULT_MPV_LOG_FILE = resolveDefaultLogFilePath({
|
||||
const DEFAULT_MPV_LOG_FILE = resolveDefaultLogFilePath('mpv', {
|
||||
platform: process.platform,
|
||||
homeDir: os.homedir(),
|
||||
appDataDir: process.env.APPDATA,
|
||||
@@ -723,7 +730,7 @@ const isDev = process.argv.includes('--dev') || process.argv.includes('--debug')
|
||||
const texthookerService = new Texthooker(() => {
|
||||
const config = getResolvedConfig();
|
||||
const characterDictionaryEnabled =
|
||||
config.anilist.characterDictionary.enabled &&
|
||||
config.subtitleStyle.nameMatchEnabled &&
|
||||
yomitanProfilePolicy.isCharacterDictionaryEnabled();
|
||||
const knownWordColoringEnabled = getRuntimeBooleanOption(
|
||||
'subtitle.annotation.knownWords.highlightEnabled',
|
||||
@@ -1295,17 +1302,24 @@ const youtubePlaybackRuntime = createYoutubePlaybackRuntime({
|
||||
await ensureYoutubePlaybackRuntimeReady();
|
||||
},
|
||||
resolveYoutubePlaybackUrl: (url, format) => resolveYoutubePlaybackUrl(url, format),
|
||||
launchWindowsMpv: (playbackUrl, args) =>
|
||||
launchWindowsMpv(
|
||||
launchWindowsMpv: (playbackUrl, args) => {
|
||||
const config = getResolvedConfig();
|
||||
setLogFileToggles(config.logging.files);
|
||||
const mpvLogPath = isLogFileEnabled('mpv') ? resolveDefaultLogFilePath('mpv') : '';
|
||||
if (mpvLogPath) {
|
||||
pruneLogDirectoryForPath(mpvLogPath, config.logging.rotation);
|
||||
}
|
||||
const mpvArgs = [...args, ...buildMpvLoggingArgs(config.logging.level, mpvLogPath, args)];
|
||||
return launchWindowsMpv(
|
||||
[playbackUrl],
|
||||
createWindowsMpvLaunchDeps({
|
||||
showError: (title, content) => dialog.showErrorBox(title, content),
|
||||
}),
|
||||
[...args, `--log-file=${DEFAULT_MPV_LOG_PATH}`],
|
||||
mpvArgs,
|
||||
process.execPath,
|
||||
resolveBundledMpvRuntimePluginEntrypoint(),
|
||||
getResolvedConfig().mpv.executablePath,
|
||||
getResolvedConfig().mpv.launchMode,
|
||||
config.mpv.executablePath,
|
||||
config.mpv.launchMode,
|
||||
{
|
||||
detectInstalledMpvPlugin: detectWindowsInstalledMpvPlugin,
|
||||
notifyInstalledPluginDetected: logInstalledMpvPluginDetected,
|
||||
@@ -1313,7 +1327,8 @@ const youtubePlaybackRuntime = createYoutubePlaybackRuntime({
|
||||
promptForLegacyMpvPluginRemovalBeforeWindowsLaunch(mpvPath, detection),
|
||||
},
|
||||
getMpvPluginRuntimeConfig(),
|
||||
),
|
||||
);
|
||||
},
|
||||
waitForYoutubeMpvConnected: (timeoutMs) => waitForYoutubeMpvConnected(timeoutMs),
|
||||
prepareYoutubePlaybackInMpv: (request) => prepareYoutubePlaybackInMpv(request),
|
||||
runYoutubePlaybackFlow: (request) => youtubeFlowRuntime.runYoutubePlaybackFlow(request),
|
||||
@@ -1705,6 +1720,7 @@ const subtitleProcessingController = createSubtitleProcessingController(
|
||||
let subtitlePrefetchService: SubtitlePrefetchService | null = null;
|
||||
let subtitlePrefetchRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let lastObservedTimePos = 0;
|
||||
let lastObservedPrimarySubtitleTrackId: number | null = null;
|
||||
let cancelLinuxMpvFullscreenOverlayRefreshBurst: CancelLinuxMpvFullscreenOverlayRefreshBurst | null =
|
||||
null;
|
||||
const SEEK_THRESHOLD_SECONDS = 3;
|
||||
@@ -1913,9 +1929,6 @@ const overlayShortcutsRuntime = createOverlayShortcutsRuntimeService(
|
||||
openRuntimeOptionsPalette: () => {
|
||||
openRuntimeOptionsPalette();
|
||||
},
|
||||
openCharacterDictionary: () => {
|
||||
openCharacterDictionaryOverlay();
|
||||
},
|
||||
openCharacterDictionaryManager: () => {
|
||||
openCharacterDictionaryManagerOverlay();
|
||||
},
|
||||
@@ -2003,6 +2016,12 @@ const buildConfigHotReloadAppliedMainDepsHandler = createBuildConfigHotReloadApp
|
||||
setLogLevel: (level) => {
|
||||
setLogLevel(level, 'config');
|
||||
},
|
||||
setLogRotation: (rotation) => {
|
||||
setLogRotation(rotation);
|
||||
},
|
||||
setLogFileToggles: (files) => {
|
||||
setLogFileToggles(files);
|
||||
},
|
||||
},
|
||||
);
|
||||
const applyConfigHotReloadDiff = createConfigHotReloadAppliedHandler(
|
||||
@@ -2217,7 +2236,7 @@ const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRunt
|
||||
const config = getResolvedConfig().anilist.characterDictionary;
|
||||
return {
|
||||
enabled:
|
||||
config.enabled &&
|
||||
getResolvedConfig().subtitleStyle.nameMatchEnabled &&
|
||||
yomitanProfilePolicy.isCharacterDictionaryEnabled() &&
|
||||
!isYoutubePlaybackActiveNow(),
|
||||
maxLoaded: config.maxLoaded,
|
||||
@@ -2931,20 +2950,21 @@ function openSessionHelpOverlay(): void {
|
||||
);
|
||||
}
|
||||
|
||||
function openCharacterDictionaryOverlay(): void {
|
||||
openOverlayHostedModalWithOsd(
|
||||
openCharacterDictionaryModalRuntime,
|
||||
'Character dictionary overlay unavailable.',
|
||||
'Failed to open character dictionary overlay.',
|
||||
);
|
||||
}
|
||||
|
||||
function openCharacterDictionaryManagerOverlay(): void {
|
||||
openOverlayHostedModalWithOsd(
|
||||
openCharacterDictionaryManagerModalRuntime,
|
||||
'Character dictionary manager unavailable.',
|
||||
'Failed to open character dictionary manager.',
|
||||
);
|
||||
openCharacterDictionaryManagerWithConfigGate({
|
||||
isCharacterDictionaryEnabled: () => getResolvedConfig().subtitleStyle.nameMatchEnabled,
|
||||
getNotificationType: () => getResolvedConfig().ankiConnect.behavior.notificationType,
|
||||
openManager: () => {
|
||||
openOverlayHostedModalWithOsd(
|
||||
openCharacterDictionaryManagerModalRuntime,
|
||||
'Character dictionary manager unavailable.',
|
||||
'Failed to open character dictionary manager.',
|
||||
);
|
||||
},
|
||||
showOsd: (message) => showMpvOsd(message),
|
||||
showDesktopNotification: (title, options) => showDesktopNotification(title, options),
|
||||
logWarn: (message, error) => logger.warn(message, error),
|
||||
});
|
||||
}
|
||||
|
||||
function openControllerSelectOverlay(): void {
|
||||
@@ -3054,7 +3074,7 @@ const {
|
||||
mpvExecutablePath: getResolvedConfig().mpv.executablePath,
|
||||
}),
|
||||
getPluginRuntimeConfig: () => getMpvPluginRuntimeConfig(),
|
||||
defaultMpvLogPath: DEFAULT_MPV_LOG_PATH,
|
||||
getDefaultMpvLogPath: () => (isLogFileEnabled('mpv') ? DEFAULT_MPV_LOG_PATH : ''),
|
||||
defaultMpvArgs: MPV_JELLYFIN_DEFAULT_ARGS,
|
||||
removeSocketPath: (socketPath) => {
|
||||
fs.rmSync(socketPath, { force: true });
|
||||
@@ -4312,6 +4332,8 @@ const { appReadyRuntimeRunner } = composeAppReadyRuntime({
|
||||
getConfigWarnings: () => configService.getWarnings(),
|
||||
logConfigWarning: (warning) => appLogger.logConfigWarning(warning),
|
||||
setLogLevel: (level: string, source: LogLevelSource) => setLogLevel(level, source),
|
||||
setLogRotation: (rotation: number) => setLogRotation(rotation),
|
||||
setLogFileToggles: (files) => setLogFileToggles(files),
|
||||
initRuntimeOptionsManager: () => {
|
||||
appState.runtimeOptionsManager = new RuntimeOptionsManager(
|
||||
() => configService.getConfig().ankiConnect,
|
||||
@@ -4638,7 +4660,11 @@ const {
|
||||
markJellyfinRemotePlaybackLoadedState(activeJellyfinRemotePlayback, path);
|
||||
},
|
||||
scheduleCharacterDictionarySync: () => {
|
||||
if (!yomitanProfilePolicy.isCharacterDictionaryEnabled() || isYoutubePlaybackActiveNow()) {
|
||||
if (
|
||||
!getResolvedConfig().subtitleStyle.nameMatchEnabled ||
|
||||
!yomitanProfilePolicy.isCharacterDictionaryEnabled() ||
|
||||
isYoutubePlaybackActiveNow()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
characterDictionaryAutoSyncRuntime.scheduleSync();
|
||||
@@ -4674,10 +4700,23 @@ const {
|
||||
);
|
||||
},
|
||||
onSubtitleTrackChange: (sid) => {
|
||||
lastObservedPrimarySubtitleTrackId = sid;
|
||||
logger.info('[mpv-subtitles] primary subtitle track changed', { sid });
|
||||
scheduleSubtitlePrefetchRefresh();
|
||||
youtubePrimarySubtitleNotificationRuntime.handleSubtitleTrackChange(sid);
|
||||
},
|
||||
onSubtitleTrackListChange: (trackList) => {
|
||||
const diagnostics = buildSubtitleTrackDiagnostics(
|
||||
lastObservedPrimarySubtitleTrackId,
|
||||
trackList,
|
||||
);
|
||||
if (!diagnostics.trackListReadable) {
|
||||
logger.warn('[mpv-subtitles] mpv reported an unreadable subtitle track list', diagnostics);
|
||||
} else if (diagnostics.subtitleTrackCount === 0) {
|
||||
logger.warn('[mpv-subtitles] mpv reported no subtitle tracks', diagnostics);
|
||||
} else {
|
||||
logger.info('[mpv-subtitles] subtitle track list updated', diagnostics);
|
||||
}
|
||||
managedLocalSubtitleSelectionRuntime.handleSubtitleTrackListChange(trackList);
|
||||
scheduleSubtitlePrefetchRefresh();
|
||||
youtubePrimarySubtitleNotificationRuntime.handleSubtitleTrackListChange(trackList);
|
||||
@@ -4763,7 +4802,7 @@ const {
|
||||
getResolvedConfig().subtitleStyle.enableJlpt,
|
||||
),
|
||||
getCharacterDictionaryEnabled: () =>
|
||||
getResolvedConfig().anilist.characterDictionary.enabled &&
|
||||
getResolvedConfig().subtitleStyle.nameMatchEnabled &&
|
||||
yomitanProfilePolicy.isCharacterDictionaryEnabled() &&
|
||||
!isYoutubePlaybackActiveNow(),
|
||||
getNameMatchEnabled: () => getResolvedConfig().subtitleStyle.nameMatchEnabled,
|
||||
@@ -5131,6 +5170,53 @@ function openYomitanSettings(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function describeUnknownError(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
async function exportLogsFromTray(): Promise<void> {
|
||||
try {
|
||||
await flushMpvLog();
|
||||
} catch (error) {
|
||||
logger.warn('Failed to flush mpv log before exporting logs from tray.', error);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = exportLogsArchive({
|
||||
platform: process.platform,
|
||||
homeDir: os.homedir(),
|
||||
appDataDir: app.getPath('appData'),
|
||||
});
|
||||
logger.info(
|
||||
`Exported ${result.exportedFiles.length} sanitized log file(s) to ${result.zipPath}`,
|
||||
);
|
||||
void dialog
|
||||
.showMessageBox({
|
||||
type: 'info',
|
||||
title: 'SubMiner logs exported',
|
||||
message: 'SubMiner log export created.',
|
||||
detail: result.zipPath,
|
||||
buttons: ['OK', 'Show in Folder'],
|
||||
defaultId: 0,
|
||||
cancelId: 0,
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.response === 1) {
|
||||
shell.showItemInFolder(result.zipPath);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
const message = describeUnknownError(error);
|
||||
logger.warn('Failed to export logs from tray.', error);
|
||||
void dialog.showMessageBox({
|
||||
type: 'error',
|
||||
title: 'SubMiner log export failed',
|
||||
message: 'Could not export SubMiner logs.',
|
||||
detail: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
getConfiguredShortcuts,
|
||||
registerGlobalShortcuts,
|
||||
@@ -5241,7 +5327,7 @@ function refreshCurrentSessionBindings(): void {
|
||||
|
||||
const { flushMpvLog, showMpvOsd } = createMpvOsdRuntimeHandlers({
|
||||
appendToMpvLogMainDeps: {
|
||||
logPath: DEFAULT_MPV_LOG_PATH,
|
||||
getLogPath: () => (isLogFileEnabled('mpv') ? DEFAULT_MPV_LOG_PATH : ''),
|
||||
dirname: (targetPath) => path.dirname(targetPath),
|
||||
mkdir: async (targetPath, options) => {
|
||||
await fs.promises.mkdir(targetPath, options);
|
||||
@@ -5757,7 +5843,6 @@ async function dispatchSessionAction(request: SessionActionDispatchRequest): Pro
|
||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||
openJimaku: () => openJimakuOverlay(),
|
||||
openSessionHelp: () => openSessionHelpOverlay(),
|
||||
openCharacterDictionary: () => openCharacterDictionaryOverlay(),
|
||||
openCharacterDictionaryManager: () => openCharacterDictionaryManagerOverlay(),
|
||||
openControllerSelect: () => openControllerSelectOverlay(),
|
||||
openControllerDebug: () => openControllerDebugOverlay(),
|
||||
@@ -6436,6 +6521,9 @@ const { ensureTray: ensureTrayHandler, destroyTray: destroyTrayHandler } =
|
||||
showWindowsMpvLauncherSetup: () => process.platform === 'win32',
|
||||
openYomitanSettings: () => openYomitanSettings(),
|
||||
openConfigSettingsWindow: () => openConfigSettingsWindow(),
|
||||
exportLogs: () => {
|
||||
void exportLogsFromTray();
|
||||
},
|
||||
openJellyfinSetupWindow: () => openJellyfinSetupWindow(),
|
||||
isJellyfinConfigured: () =>
|
||||
isJellyfinConfiguredForTrayRuntime(getJellyfinTrayDiscoveryDeps()),
|
||||
|
||||
Reference in New Issue
Block a user