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
This commit is contained in:
2026-09-02 23:28:32 -07:00
parent 68dd789fbf
commit 7bc9a07a52
6 changed files with 36 additions and 7 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ area: anime
- Added `subminer anime` and the `--anime` flag to open the browser, plus a "Browse Anime" tray entry. - Added `subminer anime` and the `--anime` flag to open the browser, plus a "Browse Anime" tray entry.
- Anime extensions are read from `<userData>/anime-extensions`; drop Aniyomi `.apk` files there to add sources. - Anime extensions are read from `<userData>/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 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. - 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`. - 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. - 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.
+1 -1
View File
@@ -629,7 +629,7 @@
// Hot-reload: autoOpenJimaku applies to the next episode; other anime changes apply the next time the anime browser opens. // Hot-reload: autoOpenJimaku applies to the next episode; other anime changes apply the next time the anime browser opens.
// ========================================== // ==========================================
"anime": { "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 <userData>/anime-extensions. "extensionsDir": "", // Directory holding Aniyomi extension .apk files. Empty uses <userData>/anime-extensions.
"repos": [], // Extension repository index URLs (any https .json index, e.g. https://.../index.min.json). Empty by default; SubMiner ships no repositories. "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. "preferredQuality": "", // Preferred stream quality label, matched as a substring (for example: 1080). Empty uses the source order.
+1 -1
View File
@@ -629,7 +629,7 @@
// Hot-reload: autoOpenJimaku applies to the next episode; other anime changes apply the next time the anime browser opens. // Hot-reload: autoOpenJimaku applies to the next episode; other anime changes apply the next time the anime browser opens.
// ========================================== // ==========================================
"anime": { "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 <userData>/anime-extensions. "extensionsDir": "", // Directory holding Aniyomi extension .apk files. Empty uses <userData>/anime-extensions.
"repos": [], // Extension repository index URLs (any https .json index, e.g. https://.../index.min.json). Empty by default; SubMiner ships no repositories. "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. "preferredQuality": "", // Preferred stream quality label, matched as a substring (for example: 1080). Empty uses the source order.
@@ -593,7 +593,7 @@ export function buildIntegrationConfigOptionRegistry(
kind: 'boolean', kind: 'boolean',
defaultValue: defaultConfig.anime.autoOpenJimaku, defaultValue: defaultConfig.anime.autoOpenJimaku,
description: 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', path: 'anime.extensionsDir',
@@ -1,6 +1,6 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; 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 { tmpdir } from 'node:os';
import path from 'node:path'; import path from 'node:path';
import type { BridgePreference } from '../../anime-bridge/types'; 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(); 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 () => { test('standalone and modal browser sessions keep source selection and searches isolated', async () => {
const client = { const client = {
listAnimeSources: async () => [{ id: 'shared', name: 'Source', lang: 'en' }], listAnimeSources: async () => [{ id: 'shared', name: 'Source', lang: 'en' }],
+4 -1
View File
@@ -632,8 +632,11 @@ export function createAnimeBrowserRuntime(deps: AnimeBrowserRuntimeDeps) {
removeExtension(pkg: string): Promise<void> { removeExtension(pkg: string): Promise<void> {
return withExtensionMutation(async () => { return withExtensionMutation(async () => {
await removeExtensionFile(deps.extensionsDir(), pkg); await removeExtensionFile(deps.extensionsDir(), pkg);
await preferenceStore.clear(pkg).catch(() => undefined); try {
await preferenceStore.clear(pkg);
} finally {
if (sidecar) await scanExtensions(sidecar); if (sidecar) await scanExtensions(sidecar);
}
}); });
}, },