mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
b1bdeabca8
* fix(jellyfin): show overlay, inject plugin, and fix stats title on playb - Show visible overlay automatically during Jellyfin playback so subtitleStyle applies - Inject bundled mpv plugin on auto-launch so keybindings work without overlay focus - Group Jellyfin playback stats under item metadata (jellyfin://host/item/id) instead of stream URLs so episodes merge with matching local titles - Mark ffsubsync unavailable in subsync modal for remote media paths - Drain queued second-instance commands even when onReady throws * fix(overlay): stabilize macOS focus handoff and sidebar Yomitan pause - Keep overlay visible during macOS foreground probe after overlay blur - Hold sidebar hover-pause while a Yomitan lookup popup remains open * fix(jellyfin): fix discovery loop, device identity, tray state, and Disc - Derive device identity from OS hostname; remove legacy configurable client/device fields - Prevent discovery playback from reloading active item, misreporting pause state, and duplicate overlay restores - Restart stale tray discovery sessions without re-login when server drops SubMiner cast target - Sync tray discovery checkbox state on Linux after CLI/startup/remote-session changes - Stop Discord presence falling back to stream URLs; prime title before tokenized stream loads - Fix picker library discovery when log level is above info - Fix config.example.jsonc trailing commas and array formatting * docs(release): trim and consolidate prerelease notes for 0.15.0 - Remove breaking changes section and several redundant bullet points - Consolidate per-platform updater notes into a single entry - Normalize em-dash separators to hyphens in section headers * fix(config): remove trailing commas from config.example.jsonc - Strip trailing commas throughout both config.example.jsonc copies - Reformat inline arrays to multi-line for JSON strictness - Update Jellyfin subtitle preload and playback launch tests and impl * fix(tokenizer): preserve known-word highlight when POS filters suppress - Known-word cache matches now set isKnown=true even for tokens excluded by POS filters - POS exclusion gate suppresses N+1, frequency, and JLPT only; known status is computed before the gate - Jellyfin subtitle preload continues after cleanup failures instead of aborting - Update config docs and option description to document the known-word bypass behavior * fix(jellyfin): send explicit hide/show overlay instead of toggle - Track overlay visibility in plugin state; y-t uses explicit hide/show commands when state is known - Prevent paused Jellyfin playback from resuming on overlay hide - Fix subtitle cache cleanup to only remove dirs after successful cleanup * fix(jellyfin): fix remote progress sync, seek reporting, and startup sto - arm active playback before loadfile with loadedMediaPath: null to suppress premature stop events - force immediate progress report on seek-like position jumps at the mpv time-pos level - send positionTicks and failed=false in reportStopped payload - remove EventName from HTTP timeline payloads (websocket-only field) - add startup grace window to drop stop events before media finishes loading * fix(jellyfin): fix overlay toggle sync, redirect reload, and AppImage bi - Sync visible-overlay state back to plugin via script messages to avoid toggle/hide drift - Collapse duplicate toggle events within 250ms to prevent hide-then-show on single keypress - Preserve manual hide across Jellyfin path-changing redirects even when media-title drops - Rearm managed subtitle defaults on path-changing redirects - Route toggleVisibleOverlay session binding through plugin toggle instead of app-side IPC - Show Linux/Hyprland overlay passively (showInactive) to avoid stealing mpv keyboard focus - Fix AppImage binary resolution to prefer $APPIMAGE env over mounted inner binary - Add stats window layer management so delete/update dialogs appear above stats window - Fix Jellyfin remote progress sync during Linux websocket reconnect windows * Fix CodeRabbit review feedback * fix(jellyfin): subtitle timing, resume progress, and overlay sync - Add per-stream subtitle delay persistence and auto timeline-offset correction - Strip server-selected subtitle stream from mpv load URL; suppress plugin subtitle rearm and auto-start during app-managed preload - Fix resume position lost when mpv resets on stop; use last known position for final progress/stopped reports - Keep Play vs Resume distinct to avoid early seek race on normal play - Fix discovery resume when remote play sends StartPositionTicks=0 despite saved progress - Deduplicate show/hide overlay commands using recorded visibility state - Rewrite docs-site Jellyfin page around cast-to-device UX * test: update lifecycle cleanup assertion * fix: clear aborted playback state, fix overlay passthrough, and guard du - Reset app_managed_playback_pending on lifecycle cleanup to prevent state leak into next item - Record visible overlay action only after command succeeds, not before - Non-native passive overlay now always click-through on re-show (fix isNonNativePassiveOverlay ordering) - Defer activeParsedSubtitleMediaPath assignment until after prefetch completes - Move autoplay gate release into the hide branch of toggleVisibleOverlay - Clear active Jellyfin playback when stopping media that never loaded - Reset managed subtitle delay and delay key when no external tracks are available - Await async removeDir in subtitle cache cleanup - Guard duplicate delete clicks in MediaDetailView and SessionsTab with refs - Escape key in DeleteConfirmDialog now calls stopPropagation and stopImmediatePropagation
404 lines
12 KiB
TypeScript
404 lines
12 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
configureEarlyAppPaths,
|
|
normalizeLaunchMpvExtraArgs,
|
|
normalizeStartupArgv,
|
|
normalizeLaunchMpvTargets,
|
|
resolveStatsDaemonCommandAction,
|
|
sanitizeHelpEnv,
|
|
sanitizeLaunchMpvEnv,
|
|
sanitizeStartupEnv,
|
|
sanitizeBackgroundEnv,
|
|
shouldDetachBackgroundLaunch,
|
|
shouldHandleHelpOnlyAtEntry,
|
|
shouldHandleLaunchMpvAtEntry,
|
|
shouldHandleStatsDaemonCommandAtEntry,
|
|
hasTransportedStartupArgs,
|
|
shouldForwardStartupArgvViaAppControl,
|
|
applyEarlyLinuxCommandLineSwitches,
|
|
resolveLinuxPasswordStoreValue,
|
|
} from './main-entry-runtime';
|
|
|
|
test('normalizeStartupArgv defaults no-arg startup to --start --background on non-Windows', () => {
|
|
const originalPlatform = process.platform;
|
|
try {
|
|
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
|
|
assert.deepEqual(normalizeStartupArgv(['SubMiner.AppImage'], {}), [
|
|
'SubMiner.AppImage',
|
|
'--start',
|
|
'--background',
|
|
]);
|
|
assert.deepEqual(
|
|
normalizeStartupArgv(['SubMiner.AppImage', '--password-store', 'gnome-libsecret'], {}),
|
|
['SubMiner.AppImage', '--password-store', 'gnome-libsecret', '--start', '--background'],
|
|
);
|
|
assert.deepEqual(normalizeStartupArgv(['SubMiner.AppImage', '--background'], {}), [
|
|
'SubMiner.AppImage',
|
|
'--background',
|
|
'--start',
|
|
]);
|
|
assert.deepEqual(normalizeStartupArgv(['SubMiner.AppImage', '--help'], {}), [
|
|
'SubMiner.AppImage',
|
|
'--help',
|
|
]);
|
|
} finally {
|
|
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
|
|
}
|
|
});
|
|
|
|
test('normalizeStartupArgv defaults no-arg Windows startup to --start only', () => {
|
|
const originalPlatform = process.platform;
|
|
try {
|
|
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
|
|
assert.deepEqual(normalizeStartupArgv(['SubMiner.exe'], {}), ['SubMiner.exe', '--start']);
|
|
} finally {
|
|
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
|
|
}
|
|
});
|
|
|
|
test('normalizeStartupArgv uses transported AppImage args instead of raw Electron args', () => {
|
|
assert.deepEqual(
|
|
normalizeStartupArgv(['SubMiner.AppImage', '--background'], {
|
|
SUBMINER_APP_ARGC: '2',
|
|
SUBMINER_APP_ARG_0: '--stop',
|
|
SUBMINER_APP_ARG_1: '--socket',
|
|
}),
|
|
['SubMiner.AppImage', '--stop', '--socket'],
|
|
);
|
|
});
|
|
|
|
test('normalizeStartupArgv defaults empty transported AppImage args to background startup', () => {
|
|
const originalPlatform = process.platform;
|
|
try {
|
|
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
|
|
assert.deepEqual(
|
|
normalizeStartupArgv(['SubMiner.AppImage', '--background'], {
|
|
SUBMINER_APP_ARGC: '0',
|
|
}),
|
|
['SubMiner.AppImage', '--start', '--background'],
|
|
);
|
|
} finally {
|
|
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
|
|
}
|
|
});
|
|
|
|
test('normalizeStartupArgv defaults passive-only transported AppImage args to background startup', () => {
|
|
const originalPlatform = process.platform;
|
|
try {
|
|
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
|
|
assert.deepEqual(
|
|
normalizeStartupArgv(['SubMiner.AppImage'], {
|
|
SUBMINER_APP_ARGC: '2',
|
|
SUBMINER_APP_ARG_0: '--password-store',
|
|
SUBMINER_APP_ARG_1: 'gnome-libsecret',
|
|
}),
|
|
['SubMiner.AppImage', '--password-store', 'gnome-libsecret', '--start', '--background'],
|
|
);
|
|
} finally {
|
|
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
|
|
}
|
|
});
|
|
|
|
test('hasTransportedStartupArgs detects env-carried app args', () => {
|
|
assert.equal(hasTransportedStartupArgs({ SUBMINER_APP_ARGC: '1' }), true);
|
|
assert.equal(hasTransportedStartupArgs({}), false);
|
|
});
|
|
|
|
test('resolveLinuxPasswordStoreValue defaults Linux safeStorage to gnome-libsecret', () => {
|
|
assert.equal(resolveLinuxPasswordStoreValue(['SubMiner.AppImage'], 'linux'), 'gnome-libsecret');
|
|
assert.equal(
|
|
resolveLinuxPasswordStoreValue(['SubMiner.AppImage', '--password-store', 'gnome'], 'linux'),
|
|
'gnome-libsecret',
|
|
);
|
|
assert.equal(resolveLinuxPasswordStoreValue(['SubMiner.exe'], 'win32'), null);
|
|
});
|
|
|
|
test('resolveLinuxPasswordStoreValue keeps scanning after a bare password-store flag', () => {
|
|
assert.equal(
|
|
resolveLinuxPasswordStoreValue(
|
|
['SubMiner.AppImage', '--password-store', '--start', '--password-store=kwallet6'],
|
|
'linux',
|
|
),
|
|
'kwallet6',
|
|
);
|
|
});
|
|
|
|
test('applyEarlyLinuxCommandLineSwitches appends password store before main startup', () => {
|
|
const switches: Array<[string, string | undefined]> = [];
|
|
applyEarlyLinuxCommandLineSwitches(
|
|
{
|
|
appendSwitch: (name, value) => {
|
|
switches.push([name, value]);
|
|
},
|
|
},
|
|
['SubMiner.AppImage', '--password-store=kwallet6'],
|
|
'linux',
|
|
);
|
|
|
|
assert.deepEqual(switches, [
|
|
['enable-features', 'GlobalShortcutsPortal'],
|
|
['password-store', 'kwallet6'],
|
|
]);
|
|
});
|
|
|
|
test('transported AppImage visibility commands should forward through app control', () => {
|
|
assert.equal(
|
|
shouldForwardStartupArgvViaAppControl(['SubMiner.AppImage', '--hide-visible-overlay'], {
|
|
SUBMINER_APP_ARGC: '1',
|
|
SUBMINER_APP_ARG_0: '--hide-visible-overlay',
|
|
}),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test('app control forwarding is only for transported runtime commands', () => {
|
|
assert.equal(
|
|
shouldForwardStartupArgvViaAppControl(['SubMiner.AppImage', '--hide-visible-overlay'], {}),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
shouldForwardStartupArgvViaAppControl(['SubMiner.AppImage', '--app-ping'], {
|
|
SUBMINER_APP_ARGC: '1',
|
|
SUBMINER_APP_ARG_0: '--app-ping',
|
|
}),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
shouldForwardStartupArgvViaAppControl(['SubMiner.AppImage', '--launch-mpv'], {
|
|
SUBMINER_APP_ARGC: '1',
|
|
SUBMINER_APP_ARG_0: '--launch-mpv',
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test('shouldHandleHelpOnlyAtEntry detects help-only invocation', () => {
|
|
assert.equal(shouldHandleHelpOnlyAtEntry(['--help'], {}), true);
|
|
assert.equal(shouldHandleHelpOnlyAtEntry(['--help', '--start'], {}), false);
|
|
assert.equal(shouldHandleHelpOnlyAtEntry(['--start'], {}), false);
|
|
assert.equal(shouldHandleHelpOnlyAtEntry(['--help'], { ELECTRON_RUN_AS_NODE: '1' }), false);
|
|
});
|
|
|
|
test('launch-mpv entry helpers detect and normalize targets', () => {
|
|
assert.equal(shouldHandleLaunchMpvAtEntry(['SubMiner.exe', '--launch-mpv'], {}), true);
|
|
assert.equal(
|
|
shouldHandleLaunchMpvAtEntry(['SubMiner.exe', '--launch-mpv'], { ELECTRON_RUN_AS_NODE: '1' }),
|
|
false,
|
|
);
|
|
assert.deepEqual(normalizeLaunchMpvTargets(['SubMiner.exe', '--launch-mpv']), []);
|
|
assert.deepEqual(normalizeLaunchMpvTargets(['SubMiner.exe', '--launch-mpv', 'C:\\a.mkv']), [
|
|
'C:\\a.mkv',
|
|
]);
|
|
assert.deepEqual(
|
|
normalizeLaunchMpvExtraArgs([
|
|
'SubMiner.exe',
|
|
'--launch-mpv',
|
|
'--sub-file',
|
|
'track.srt',
|
|
'C:\\a.mkv',
|
|
]),
|
|
['--sub-file', 'track.srt'],
|
|
);
|
|
assert.deepEqual(
|
|
normalizeLaunchMpvTargets([
|
|
'SubMiner.exe',
|
|
'--launch-mpv',
|
|
'--sub-file',
|
|
'track.srt',
|
|
'C:\\a.mkv',
|
|
]),
|
|
['C:\\a.mkv'],
|
|
);
|
|
assert.deepEqual(
|
|
normalizeLaunchMpvExtraArgs([
|
|
'SubMiner.exe',
|
|
'--launch-mpv',
|
|
'--profile=subminer',
|
|
'--pause=yes',
|
|
'C:\\a.mkv',
|
|
]),
|
|
['--profile=subminer', '--pause=yes'],
|
|
);
|
|
assert.deepEqual(
|
|
normalizeLaunchMpvExtraArgs([
|
|
'SubMiner.exe',
|
|
'--launch-mpv',
|
|
'--input-ipc-server',
|
|
'\\\\.\\pipe\\custom-subminer-socket',
|
|
'--alang',
|
|
'ja,jpn',
|
|
'C:\\a.mkv',
|
|
]),
|
|
['--input-ipc-server', '\\\\.\\pipe\\custom-subminer-socket', '--alang', 'ja,jpn'],
|
|
);
|
|
assert.deepEqual(
|
|
normalizeLaunchMpvExtraArgs(['SubMiner.exe', '--launch-mpv', '--fullscreen', 'C:\\a.mkv']),
|
|
['--fullscreen'],
|
|
);
|
|
assert.deepEqual(
|
|
normalizeLaunchMpvTargets([
|
|
'SubMiner.exe',
|
|
'--launch-mpv',
|
|
'--input-ipc-server',
|
|
'\\\\.\\pipe\\custom-subminer-socket',
|
|
'--alang',
|
|
'ja,jpn',
|
|
'C:\\a.mkv',
|
|
'C:\\b.mkv',
|
|
]),
|
|
['C:\\a.mkv', 'C:\\b.mkv'],
|
|
);
|
|
assert.deepEqual(
|
|
normalizeLaunchMpvTargets(['SubMiner.exe', '--launch-mpv', '--fullscreen', 'C:\\a.mkv']),
|
|
['C:\\a.mkv'],
|
|
);
|
|
assert.deepEqual(
|
|
normalizeLaunchMpvExtraArgs([
|
|
'SubMiner.exe',
|
|
'--launch-mpv',
|
|
'--msg-level',
|
|
'all=warn',
|
|
'C:\\a.mkv',
|
|
]),
|
|
['--msg-level', 'all=warn'],
|
|
);
|
|
});
|
|
|
|
test('stats-daemon entry helper detects internal daemon commands', () => {
|
|
assert.equal(
|
|
shouldHandleStatsDaemonCommandAtEntry(['SubMiner.AppImage', '--stats-daemon-start'], {}),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
shouldHandleStatsDaemonCommandAtEntry(['SubMiner.AppImage', '--stats-daemon-stop'], {}),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
shouldHandleStatsDaemonCommandAtEntry(['SubMiner.AppImage', '--stats-daemon-start'], {
|
|
ELECTRON_RUN_AS_NODE: '1',
|
|
}),
|
|
false,
|
|
);
|
|
assert.equal(shouldHandleStatsDaemonCommandAtEntry(['SubMiner.AppImage', '--start'], {}), false);
|
|
});
|
|
|
|
test('stats-daemon entry helper detects public background stats commands', () => {
|
|
assert.equal(
|
|
shouldHandleStatsDaemonCommandAtEntry(
|
|
['SubMiner.AppImage', '--stats', '--stats-background'],
|
|
{},
|
|
),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
shouldHandleStatsDaemonCommandAtEntry(['SubMiner.AppImage', '--stats', '--stats-stop'], {}),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
shouldHandleStatsDaemonCommandAtEntry(['SubMiner.AppImage', '--stats-background'], {}),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
shouldHandleStatsDaemonCommandAtEntry(['SubMiner.AppImage', '--stats-background'], {
|
|
ELECTRON_RUN_AS_NODE: '1',
|
|
}),
|
|
false,
|
|
);
|
|
assert.equal(shouldHandleStatsDaemonCommandAtEntry(['SubMiner.AppImage', '--stats'], {}), false);
|
|
});
|
|
|
|
test('stats-daemon entry helper resolves daemon action for public and internal commands', () => {
|
|
assert.equal(
|
|
resolveStatsDaemonCommandAction(['SubMiner.AppImage', '--stats-daemon-start']),
|
|
'start',
|
|
);
|
|
assert.equal(
|
|
resolveStatsDaemonCommandAction(['SubMiner.AppImage', '--stats-daemon-stop']),
|
|
'stop',
|
|
);
|
|
assert.equal(
|
|
resolveStatsDaemonCommandAction(['SubMiner.AppImage', '--stats', '--stats-background']),
|
|
'start',
|
|
);
|
|
assert.equal(
|
|
resolveStatsDaemonCommandAction(['SubMiner.AppImage', '--stats', '--stats-stop']),
|
|
'stop',
|
|
);
|
|
assert.equal(resolveStatsDaemonCommandAction(['SubMiner.AppImage', '--stats']), null);
|
|
});
|
|
|
|
test('sanitizeStartupEnv suppresses warnings and lsfg layer', () => {
|
|
const env = sanitizeStartupEnv({
|
|
VK_INSTANCE_LAYERS: 'foo:lsfg-vk:bar',
|
|
});
|
|
assert.equal(env.NODE_NO_WARNINGS, '1');
|
|
assert.equal('VK_INSTANCE_LAYERS' in env, false);
|
|
});
|
|
|
|
test('sanitizeHelpEnv suppresses warnings and lsfg layer', () => {
|
|
const env = sanitizeHelpEnv({
|
|
VK_INSTANCE_LAYERS: 'foo:lsfg-vk:bar',
|
|
});
|
|
assert.equal(env.NODE_NO_WARNINGS, '1');
|
|
assert.equal('VK_INSTANCE_LAYERS' in env, false);
|
|
});
|
|
|
|
test('sanitizeLaunchMpvEnv suppresses warnings and lsfg layer', () => {
|
|
const env = sanitizeLaunchMpvEnv({
|
|
VK_INSTANCE_LAYERS: 'foo:lsfg-vk:bar',
|
|
});
|
|
assert.equal(env.NODE_NO_WARNINGS, '1');
|
|
assert.equal('VK_INSTANCE_LAYERS' in env, false);
|
|
});
|
|
|
|
test('sanitizeBackgroundEnv marks background child and keeps warning suppression', () => {
|
|
const env = sanitizeBackgroundEnv({
|
|
VK_INSTANCE_LAYERS: 'foo:lsfg-vk:bar',
|
|
});
|
|
assert.equal(env.SUBMINER_BACKGROUND_CHILD, '1');
|
|
assert.equal(env.NODE_NO_WARNINGS, '1');
|
|
assert.equal('VK_INSTANCE_LAYERS' in env, false);
|
|
});
|
|
|
|
test('shouldDetachBackgroundLaunch only for first background invocation', () => {
|
|
assert.equal(shouldDetachBackgroundLaunch(['--background'], {}), true);
|
|
assert.equal(
|
|
shouldDetachBackgroundLaunch(['--background'], { SUBMINER_BACKGROUND_CHILD: '1' }),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
shouldDetachBackgroundLaunch(['--background'], { ELECTRON_RUN_AS_NODE: '1' }),
|
|
false,
|
|
);
|
|
assert.equal(shouldDetachBackgroundLaunch(['--start'], {}), false);
|
|
});
|
|
|
|
test('configureEarlyAppPaths pins userData to canonical SubMiner config dir', () => {
|
|
const calls: string[] = [];
|
|
|
|
const userDataPath = configureEarlyAppPaths(
|
|
{
|
|
setName: (name) => {
|
|
calls.push(`name:${name}`);
|
|
},
|
|
setPath: (key, value) => {
|
|
calls.push(`path:${key}:${value}`);
|
|
},
|
|
},
|
|
{
|
|
platform: 'linux',
|
|
homeDir: '/home/tester',
|
|
xdgConfigHome: '/tmp/xdg',
|
|
existsSync: (candidate) => candidate === '/tmp/xdg/subminer/config.jsonc',
|
|
},
|
|
);
|
|
|
|
assert.equal(userDataPath, '/tmp/xdg/SubMiner');
|
|
assert.deepEqual(calls, ['name:SubMiner', 'path:userData:/tmp/xdg/SubMiner']);
|
|
});
|