mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-21 17:16:20 -07:00
fix(anime): clarify bridge failures and update guidance
- Show actionable bridge errors and preserve episodes when details fail - Check external bridges for updates without offering unsafe in-app installs - Complete cleanup and ignore callbacks from closed Tsukihime sessions
This commit is contained in:
@@ -182,7 +182,7 @@ test('with nothing installed the newest release is downloaded and marked', async
|
||||
assert.ok(!(await readdir(managed)).some((entry) => entry.endsWith('.zip')));
|
||||
});
|
||||
|
||||
test('findBridgeUpdate offers the newest release only to a managed install that is behind it', async () => {
|
||||
test('findBridgeUpdate compares both managed and system installs with upstream', async () => {
|
||||
const { calls, options } = fakeUpstream();
|
||||
|
||||
assert.equal(await findBridgeUpdate({ origin: 'managed', version: 'v1.0.6.0' }, options), LATEST);
|
||||
@@ -192,9 +192,12 @@ test('findBridgeUpdate offers the newest release only to a managed install that
|
||||
assert.equal(await findBridgeUpdate({ origin: 'managed', version: null }, options), LATEST);
|
||||
assert.equal(calls.length, 4);
|
||||
|
||||
// A system install is pacman's, so upstream is not even asked.
|
||||
assert.equal(await findBridgeUpdate({ origin: 'system', version: 'v1.0.0.0' }, options), null);
|
||||
assert.equal(calls.length, 4);
|
||||
assert.equal(await findBridgeUpdate({ origin: 'system', version: 'v1.0.0.0' }, options), LATEST);
|
||||
assert.equal(await findBridgeUpdate({ origin: 'system', version: LATEST }, options), null);
|
||||
assert.equal(await findBridgeUpdate({ origin: 'system', version: 'v1.0.7.0' }, options), null);
|
||||
// An unknown external version is not evidence that an update is needed.
|
||||
assert.equal(await findBridgeUpdate({ origin: 'system', version: null }, options), null);
|
||||
assert.equal(calls.length, 7);
|
||||
});
|
||||
|
||||
test('findBridgeUpdate propagates a failed release listing', async () => {
|
||||
|
||||
@@ -178,16 +178,16 @@ async function locateLatestBundle(options: BridgeReleaseOptions): Promise<Bundle
|
||||
}
|
||||
|
||||
/**
|
||||
* The newest release a managed install could move to, or null when it is
|
||||
* current or is not SubMiner's to update. An install whose version cannot be
|
||||
* read is offered the newest release: re-downloading is the way back to a
|
||||
* known state. Network errors propagate; the caller decides how loudly.
|
||||
* The newest release an install could move to, or null when it is current.
|
||||
* An unreadable managed install is offered the newest release so it can be
|
||||
* repaired; an unreadable system install cannot be compared. Network errors
|
||||
* propagate; the caller decides how loudly.
|
||||
*/
|
||||
export async function findBridgeUpdate(
|
||||
install: Pick<AnimeBrowserBridgeInstall, 'origin' | 'version'>,
|
||||
options: BridgeReleaseOptions = {},
|
||||
): Promise<string | null> {
|
||||
if (install.origin !== 'managed') return null;
|
||||
if (install.origin === 'system' && install.version === null) return null;
|
||||
const latest = await locateLatestBundle(options);
|
||||
if (install.version === null) return latest.tagName;
|
||||
return compareBundleVersions(latest.tagName, install.version) > 0 ? latest.tagName : null;
|
||||
|
||||
@@ -107,19 +107,28 @@ test('a failed update check is logged and leaves the bridge ready', async () =>
|
||||
assert.ok(logged.some((line) => /update check failed: rate limited/.test(line)));
|
||||
});
|
||||
|
||||
test('a system install is never asked about updates', async () => {
|
||||
test('a system install broadcasts available updates without allowing installation', async () => {
|
||||
let asked = 0;
|
||||
const { runtime } = await setup({
|
||||
let staged = false;
|
||||
const { runtime, states, stopped } = await setup({
|
||||
ensureBinaries: async () => ({ ...OLD, origin: 'system' }),
|
||||
checkBridgeUpdate: async () => {
|
||||
asked += 1;
|
||||
return LATEST;
|
||||
},
|
||||
stageBridgeUpdate: async () => {
|
||||
staged = true;
|
||||
throw new Error('must not stage a system bridge update');
|
||||
},
|
||||
});
|
||||
await runtime.ensureBridge();
|
||||
await tick();
|
||||
assert.equal(asked, 0);
|
||||
assert.equal(runtime.getSnapshot().bridge.install?.updateAvailable, null);
|
||||
assert.equal(asked, 1);
|
||||
assert.equal(runtime.getSnapshot().bridge.install?.updateAvailable, LATEST);
|
||||
assert.equal(states.at(-1)?.install?.updateAvailable, LATEST);
|
||||
await assert.rejects(runtime.updateBridge(), /managed outside SubMiner/);
|
||||
assert.equal(staged, false);
|
||||
assert.deepEqual(stopped, []);
|
||||
});
|
||||
|
||||
test('updateBridge stages, stops the old bridge, and restarts on the new install', async () => {
|
||||
|
||||
@@ -218,12 +218,12 @@ export function createAnimeBrowserRuntime(deps: AnimeBrowserRuntimeDeps) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask upstream whether a managed install is behind, after the bridge is up
|
||||
* Ask upstream whether the install is behind, after the bridge is up
|
||||
* so a slow or failed GitHub call never delays a search. The answer lands
|
||||
* in `install.updateAvailable` and is re-broadcast on the current state.
|
||||
*/
|
||||
async function checkForBridgeUpdate(handle: SidecarHandle): Promise<void> {
|
||||
if (install === null || install.origin !== 'managed') return;
|
||||
if (install === null) return;
|
||||
try {
|
||||
const latest = await deps.checkBridgeUpdate(install);
|
||||
// The bridge may have been restarted or updated while we waited.
|
||||
|
||||
@@ -67,57 +67,79 @@ test('on will quit cleanup handler runs all cleanup steps', async () => {
|
||||
assert.ok(calls.indexOf('flush-mpv-log') < calls.indexOf('destroy-socket'));
|
||||
});
|
||||
|
||||
test('on will quit cleanup handler cleans jellyfin subtitle cache when stopping remote session fails', async () => {
|
||||
const calls: string[] = [];
|
||||
const cleanup = createOnWillQuitCleanupHandler({
|
||||
destroyTray: () => {},
|
||||
stopConfigHotReload: () => {},
|
||||
restorePreviousSecondarySubVisibility: () => {},
|
||||
restoreMpvSubVisibility: () => {},
|
||||
unregisterAllGlobalShortcuts: () => {},
|
||||
stopSubtitleWebsocket: () => {},
|
||||
stopTexthookerService: () => {},
|
||||
stopSyncAutoScheduler: () => {},
|
||||
clearWindowsVisibleOverlayForegroundPollLoop: () => {},
|
||||
clearLinuxMpvFullscreenOverlayRefreshTimeouts: () => {},
|
||||
destroyMainOverlayWindow: () => {},
|
||||
destroyModalOverlayWindow: () => {},
|
||||
destroyYomitanParserWindow: () => {},
|
||||
clearYomitanParserState: () => {},
|
||||
stopWindowTracker: () => {},
|
||||
flushMpvLog: () => {},
|
||||
destroyMpvSocket: () => {},
|
||||
clearReconnectTimer: () => {},
|
||||
destroySubtitleTimingTracker: () => {},
|
||||
destroyImmersionTracker: () => {},
|
||||
destroyAnkiIntegration: () => {},
|
||||
destroyAnilistSetupWindow: () => {},
|
||||
clearAnilistSetupWindow: () => {},
|
||||
destroyJellyfinSetupWindow: () => {},
|
||||
clearJellyfinSetupWindow: () => {},
|
||||
destroyFirstRunSetupWindow: () => {},
|
||||
clearFirstRunSetupWindow: () => {},
|
||||
destroyYomitanSettingsWindow: () => {},
|
||||
clearYomitanSettingsWindow: () => {},
|
||||
stopJellyfinRemoteSession: () => {
|
||||
calls.push('stop-jellyfin-remote');
|
||||
throw new Error('stop failed');
|
||||
},
|
||||
cleanupInternalSubtitleTrackCache: () => calls.push('cleanup-internal-subtitles'),
|
||||
cleanupYoutubeSubtitleTempDirs: () => calls.push('cleanup-youtube-subtitles'),
|
||||
cleanupYoutubeMediaCache: () => calls.push('cleanup-youtube-media'),
|
||||
cleanupRemoteMediaWindows: () => calls.push('cleanup-remote-media-windows'),
|
||||
cleanupJellyfinSubtitleCache: () => calls.push('cleanup-jellyfin-subtitles'),
|
||||
stopDiscordPresenceService: () => calls.push('stop-discord-presence'),
|
||||
});
|
||||
for (const failedStep of [
|
||||
'stop-jellyfin-remote',
|
||||
'cleanup-jellyfin-subtitles',
|
||||
'cleanup-internal-subtitles',
|
||||
'cleanup-youtube-subtitles',
|
||||
'cleanup-youtube-media',
|
||||
'cleanup-remote-media-windows',
|
||||
'stop-discord-presence',
|
||||
'stop-sync-auto-scheduler',
|
||||
]) {
|
||||
test(`on will quit cleanup finishes every independent step after ${failedStep} fails`, async () => {
|
||||
const calls: string[] = [];
|
||||
const firstError = new Error(`${failedStep} failed`);
|
||||
const recordCleanup = (step: string): void => {
|
||||
calls.push(step);
|
||||
if (step === failedStep) throw firstError;
|
||||
if (calls.includes(failedStep)) throw new Error(`${step} also failed`);
|
||||
};
|
||||
const cleanup = createOnWillQuitCleanupHandler({
|
||||
destroyTray: () => {},
|
||||
stopConfigHotReload: () => {},
|
||||
restorePreviousSecondarySubVisibility: () => {},
|
||||
restoreMpvSubVisibility: () => {},
|
||||
unregisterAllGlobalShortcuts: () => {},
|
||||
stopSubtitleWebsocket: () => {},
|
||||
stopTexthookerService: () => {},
|
||||
stopSyncAutoScheduler: async () => {
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
||||
recordCleanup('stop-sync-auto-scheduler');
|
||||
},
|
||||
clearWindowsVisibleOverlayForegroundPollLoop: () => {},
|
||||
clearLinuxMpvFullscreenOverlayRefreshTimeouts: () => {},
|
||||
destroyMainOverlayWindow: () => {},
|
||||
destroyModalOverlayWindow: () => {},
|
||||
destroyYomitanParserWindow: () => {},
|
||||
clearYomitanParserState: () => {},
|
||||
stopWindowTracker: () => {},
|
||||
flushMpvLog: () => {},
|
||||
destroyMpvSocket: () => {},
|
||||
clearReconnectTimer: () => {},
|
||||
destroySubtitleTimingTracker: () => {},
|
||||
destroyImmersionTracker: () => {},
|
||||
destroyAnkiIntegration: () => {},
|
||||
destroyAnilistSetupWindow: () => {},
|
||||
clearAnilistSetupWindow: () => {},
|
||||
destroyJellyfinSetupWindow: () => {},
|
||||
clearJellyfinSetupWindow: () => {},
|
||||
destroyFirstRunSetupWindow: () => {},
|
||||
clearFirstRunSetupWindow: () => {},
|
||||
destroyYomitanSettingsWindow: () => {},
|
||||
clearYomitanSettingsWindow: () => {},
|
||||
stopJellyfinRemoteSession: () => recordCleanup('stop-jellyfin-remote'),
|
||||
cleanupInternalSubtitleTrackCache: () => recordCleanup('cleanup-internal-subtitles'),
|
||||
cleanupYoutubeSubtitleTempDirs: () => recordCleanup('cleanup-youtube-subtitles'),
|
||||
cleanupYoutubeMediaCache: () => recordCleanup('cleanup-youtube-media'),
|
||||
cleanupRemoteMediaWindows: () => recordCleanup('cleanup-remote-media-windows'),
|
||||
cleanupJellyfinSubtitleCache: () => recordCleanup('cleanup-jellyfin-subtitles'),
|
||||
stopDiscordPresenceService: () => recordCleanup('stop-discord-presence'),
|
||||
});
|
||||
|
||||
await assert.rejects(cleanup(), /stop failed/);
|
||||
assert.deepEqual(calls, [
|
||||
'stop-jellyfin-remote',
|
||||
'cleanup-jellyfin-subtitles',
|
||||
'cleanup-internal-subtitles',
|
||||
]);
|
||||
});
|
||||
await assert.rejects(cleanup(), (error) => error === firstError);
|
||||
assert.deepEqual(calls, [
|
||||
'stop-jellyfin-remote',
|
||||
'cleanup-jellyfin-subtitles',
|
||||
'cleanup-internal-subtitles',
|
||||
'cleanup-youtube-subtitles',
|
||||
'cleanup-youtube-media',
|
||||
'cleanup-remote-media-windows',
|
||||
'stop-discord-presence',
|
||||
'stop-sync-auto-scheduler',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
test('should restore windows on activate requires initialized runtime and no windows', () => {
|
||||
let initialized = false;
|
||||
|
||||
@@ -37,6 +37,7 @@ export function createOnWillQuitCleanupHandler(deps: {
|
||||
stopDiscordPresenceService: () => void;
|
||||
}) {
|
||||
return async (): Promise<void> => {
|
||||
const cleanupErrors: unknown[] = [];
|
||||
deps.destroyTray();
|
||||
deps.stopConfigHotReload();
|
||||
deps.restorePreviousSecondarySubVisibility();
|
||||
@@ -44,7 +45,11 @@ export function createOnWillQuitCleanupHandler(deps: {
|
||||
deps.unregisterAllGlobalShortcuts();
|
||||
deps.stopSubtitleWebsocket();
|
||||
deps.stopTexthookerService();
|
||||
const stopSyncAutoScheduler = deps.stopSyncAutoScheduler();
|
||||
const stopSyncAutoScheduler = Promise.resolve(deps.stopSyncAutoScheduler()).catch(
|
||||
(error: unknown) => {
|
||||
cleanupErrors.push(error);
|
||||
},
|
||||
);
|
||||
deps.clearWindowsVisibleOverlayForegroundPollLoop();
|
||||
deps.clearLinuxMpvFullscreenOverlayRefreshTimeouts();
|
||||
deps.destroyMainOverlayWindow();
|
||||
@@ -66,20 +71,25 @@ export function createOnWillQuitCleanupHandler(deps: {
|
||||
deps.clearFirstRunSetupWindow();
|
||||
deps.destroyYomitanSettingsWindow();
|
||||
deps.clearYomitanSettingsWindow();
|
||||
try {
|
||||
deps.stopJellyfinRemoteSession();
|
||||
} finally {
|
||||
const runCleanup = (cleanup: () => void): void => {
|
||||
try {
|
||||
deps.cleanupJellyfinSubtitleCache();
|
||||
} finally {
|
||||
deps.cleanupInternalSubtitleTrackCache();
|
||||
cleanup();
|
||||
} catch (error) {
|
||||
cleanupErrors.push(error);
|
||||
}
|
||||
};
|
||||
try {
|
||||
runCleanup(deps.stopJellyfinRemoteSession);
|
||||
runCleanup(deps.cleanupJellyfinSubtitleCache);
|
||||
runCleanup(deps.cleanupInternalSubtitleTrackCache);
|
||||
} finally {
|
||||
runCleanup(deps.cleanupYoutubeSubtitleTempDirs);
|
||||
runCleanup(deps.cleanupYoutubeMediaCache);
|
||||
runCleanup(deps.cleanupRemoteMediaWindows);
|
||||
runCleanup(deps.stopDiscordPresenceService);
|
||||
await stopSyncAutoScheduler;
|
||||
}
|
||||
deps.cleanupYoutubeSubtitleTempDirs();
|
||||
deps.cleanupYoutubeMediaCache();
|
||||
deps.cleanupRemoteMediaWindows();
|
||||
deps.stopDiscordPresenceService();
|
||||
await stopSyncAutoScheduler;
|
||||
if (cleanupErrors.length > 0) throw cleanupErrors[0];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user