From 7bc9a07a523389c7045418a178c9b3ecaa0a2c84 Mon Sep 17 00:00:00 2001 From: sudacode Date: Wed, 2 Sep 2026 23:28:32 -0700 Subject: [PATCH] fix(anime): rescan runtime after preference cleanup failures - Keep extension removal reflected in the runtime when preference cleanup fails - Clarify auto-open playback resume behavior in configuration docs --- changes/anime-browser.md | 2 +- config.example.jsonc | 2 +- docs-site/public/config.example.jsonc | 2 +- .../definitions/options-integrations.ts | 2 +- .../anime-browser-runtime-preferences.test.ts | 28 ++++++++++++++++++- src/main/runtime/anime-browser-runtime.ts | 7 +++-- 6 files changed, 36 insertions(+), 7 deletions(-) diff --git a/changes/anime-browser.md b/changes/anime-browser.md index 57d1037f..871f3a2d 100644 --- a/changes/anime-browser.md +++ b/changes/anime-browser.md @@ -5,7 +5,7 @@ area: anime - Added `subminer anime` and the `--anime` flag to open the browser, plus a "Browse Anime" tray entry. - Anime extensions are read from `/anime-extensions`; drop Aniyomi `.apk` files there to add sources. - Added a source settings tab so extensions that need configuration (server address, credentials, quality) can be set up from the browser; values persist per extension and source, and updated extension schemas replace stale saved field definitions without losing values. Older unscoped preferences are discarded once because their package ownership cannot be proven safely. -- Added an Extensions tab for adding repository URLs and installing, updating, or removing extensions in place; extensions that fail to load are listed with the reason. Repository requests time out instead of hanging, and APK updates are staged before replacing the installed copy. +- Added an Extensions tab for adding repository URLs and installing, updating, or removing extensions in place; extensions that fail to load are listed with the reason. Repository requests time out instead of hanging, APK updates are staged before replacing the installed copy, and removing an extension refreshes the runtime even when saved credential cleanup fails while still reporting that failure. - Browse, Extensions, and Source settings are tabs, so each one gets the full window instead of sharing it with the search results. - Repository URLs only need to be an https URL to a `.json` index; the file name is not restricted to `index.min.json`. - The source picker offers "All sources", which searches every installed source at once. Results stream in as each source answers, with per-source progress in the status bar. Results are tagged with their source, failures do not blank the grid, and **Load more** appends later pages without duplicating streamed entries. diff --git a/config.example.jsonc b/config.example.jsonc index b1477155..c709b9e0 100644 --- a/config.example.jsonc +++ b/config.example.jsonc @@ -629,7 +629,7 @@ // Hot-reload: autoOpenJimaku applies to the next episode; other anime changes apply the next time the anime browser opens. // ========================================== "anime": { - "autoOpenJimaku": false, // Pause Anime Browser playback and open Jimaku when an episode loads. Playback resumes after a subtitle loads or the modal closes. Values: true | false + "autoOpenJimaku": false, // Pause Anime Browser playback and open Jimaku when an episode loads. Playback resumes after a subtitle loads or the modal closes only when auto-open initiated the pause. Values: true | false "extensionsDir": "", // Directory holding Aniyomi extension .apk files. Empty uses /anime-extensions. "repos": [], // Extension repository index URLs (any https .json index, e.g. https://.../index.min.json). Empty by default; SubMiner ships no repositories. "preferredQuality": "", // Preferred stream quality label, matched as a substring (for example: 1080). Empty uses the source order. diff --git a/docs-site/public/config.example.jsonc b/docs-site/public/config.example.jsonc index b1477155..c709b9e0 100644 --- a/docs-site/public/config.example.jsonc +++ b/docs-site/public/config.example.jsonc @@ -629,7 +629,7 @@ // Hot-reload: autoOpenJimaku applies to the next episode; other anime changes apply the next time the anime browser opens. // ========================================== "anime": { - "autoOpenJimaku": false, // Pause Anime Browser playback and open Jimaku when an episode loads. Playback resumes after a subtitle loads or the modal closes. Values: true | false + "autoOpenJimaku": false, // Pause Anime Browser playback and open Jimaku when an episode loads. Playback resumes after a subtitle loads or the modal closes only when auto-open initiated the pause. Values: true | false "extensionsDir": "", // Directory holding Aniyomi extension .apk files. Empty uses /anime-extensions. "repos": [], // Extension repository index URLs (any https .json index, e.g. https://.../index.min.json). Empty by default; SubMiner ships no repositories. "preferredQuality": "", // Preferred stream quality label, matched as a substring (for example: 1080). Empty uses the source order. diff --git a/src/config/definitions/options-integrations.ts b/src/config/definitions/options-integrations.ts index 20d0f6b4..61ca35f9 100644 --- a/src/config/definitions/options-integrations.ts +++ b/src/config/definitions/options-integrations.ts @@ -593,7 +593,7 @@ export function buildIntegrationConfigOptionRegistry( kind: 'boolean', defaultValue: defaultConfig.anime.autoOpenJimaku, description: - 'Pause Anime Browser playback and open Jimaku when an episode loads. Playback resumes after a subtitle loads or the modal closes.', + 'Pause Anime Browser playback and open Jimaku when an episode loads. Playback resumes after a subtitle loads or the modal closes only when auto-open initiated the pause.', }, { path: 'anime.extensionsDir', diff --git a/src/main/runtime/anime-browser-runtime-preferences.test.ts b/src/main/runtime/anime-browser-runtime-preferences.test.ts index 05748bcf..389f1138 100644 --- a/src/main/runtime/anime-browser-runtime-preferences.test.ts +++ b/src/main/runtime/anime-browser-runtime-preferences.test.ts @@ -1,6 +1,6 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { mkdtemp, readFile, writeFile } from 'node:fs/promises'; +import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import type { BridgePreference } from '../../anime-bridge/types'; @@ -145,6 +145,32 @@ test('colliding bridge ids keep package preferences isolated and uninstall clear await runtime.dispose(); }); +test('uninstall rescans after preference cleanup fails and propagates the failure', async () => { + const saved = textPreference('password', 'secret'); + const client = { + listAnimeSources: async () => [{ id: 'shared', name: 'Source', lang: 'en' }], + getSourcePreferences: async () => [textPreference('password')], + }; + const { runtime, preferencesFile } = await setupRuntime( + client, + { 'pkg.one': 'one' }, + { 'pkg.one:shared': [saved] }, + ); + await runtime.getPreferences('pkg.one:shared'); + + const blockedTemporaryFile = `${preferencesFile}.tmp`; + await mkdir(blockedTemporaryFile); + try { + await assert.rejects(runtime.removeExtension('pkg.one')); + assert.deepEqual(runtime.getSnapshot().installed, []); + assert.deepEqual(runtime.getSnapshot().sources, []); + } finally { + await chmod(blockedTemporaryFile, 0o700); + await rm(blockedTemporaryFile, { recursive: true, force: true }); + await runtime.dispose(); + } +}); + test('standalone and modal browser sessions keep source selection and searches isolated', async () => { const client = { listAnimeSources: async () => [{ id: 'shared', name: 'Source', lang: 'en' }], diff --git a/src/main/runtime/anime-browser-runtime.ts b/src/main/runtime/anime-browser-runtime.ts index 2af7b94e..a4354625 100644 --- a/src/main/runtime/anime-browser-runtime.ts +++ b/src/main/runtime/anime-browser-runtime.ts @@ -632,8 +632,11 @@ export function createAnimeBrowserRuntime(deps: AnimeBrowserRuntimeDeps) { removeExtension(pkg: string): Promise { return withExtensionMutation(async () => { await removeExtensionFile(deps.extensionsDir(), pkg); - await preferenceStore.clear(pkg).catch(() => undefined); - if (sidecar) await scanExtensions(sidecar); + try { + await preferenceStore.clear(pkg); + } finally { + if (sidecar) await scanExtensions(sidecar); + } }); },