mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
feat(anilist): add CLI and IPC management controls
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
---
|
||||
id: TASK-29
|
||||
title: Add Anilist integration for post-watch updates
|
||||
status: In Progress
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: '2026-02-13 17:57'
|
||||
updated_date: '2026-02-17 04:19'
|
||||
updated_date: '2026-02-17 09:27'
|
||||
labels:
|
||||
- anilist
|
||||
- anime
|
||||
@@ -32,26 +32,36 @@ Requirements:
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 Application can authenticate with Anilist and store tokens securely for desktop user sessions.
|
||||
- [ ] #2 Anilist update flow from existing local watch/session metadata can update animes watched progress after watching.
|
||||
- [ ] #3 Core functionality equivalent to mpv-anilist-updater is implemented for this use case (progress/status sync) inside the Electron app.
|
||||
- [ ] #4 A background/in-process queue handles transient API failures and retries without losing updates.
|
||||
- [ ] #5 Sync updates are non-blocking and do not degrade normal playback/mining behavior.
|
||||
- [ ] #6 Anilist integration code is modularized to allow future feature additions without major refactor (clear service boundaries/interfaces).
|
||||
- [ ] #7 Error states and duplicate/duplicate-inconsistent updates are handled deterministically (idempotent where practical).
|
||||
- [x] #1 Application can authenticate with Anilist and store tokens securely for desktop user sessions.
|
||||
- [x] #2 Anilist update flow from existing local watch/session metadata can update animes watched progress after watching.
|
||||
- [x] #3 Core functionality equivalent to mpv-anilist-updater is implemented for this use case (progress/status sync) inside the Electron app.
|
||||
- [x] #4 A background/in-process queue handles transient API failures and retries without losing updates.
|
||||
- [x] #5 Sync updates are non-blocking and do not degrade normal playback/mining behavior.
|
||||
- [x] #6 Anilist integration code is modularized to allow future feature additions without major refactor (clear service boundaries/interfaces).
|
||||
- [x] #7 Error states and duplicate/duplicate-inconsistent updates are handled deterministically (idempotent where practical).
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Completed child tasks TASK-29.1 and TASK-29.2: secure token persistence/fallback and persistent retry queue with backoff/dead-letter are now implemented.
|
||||
|
||||
Implemented AniList control surfaces across CLI and IPC. CLI: added `--anilist-status`, `--anilist-logout`, `--anilist-setup`, `--anilist-retry-queue` parsing/help/dispatch/runtime wiring in `src/cli/args.ts`, `src/cli/help.ts`, `src/core/services/cli-command.ts`, `src/main/cli-runtime.ts`, and `src/main.ts`. IPC: added `anilist:get-status`, `anilist:clear-token`, `anilist:open-setup`, `anilist:get-queue-status`, `anilist:retry-now` handlers and dependency wiring in `src/core/services/ipc.ts`, `src/main/dependencies.ts`, and `src/main.ts`. Added retry queue tests in `src/core/services/anilist/anilist-update-queue.test.ts`; added token-store tests in `src/core/services/anilist/anilist-token-store.test.ts` with runtime guard for environments without Electron safeStorage; updated `package.json` test list and AniList command/channel docs in `docs/configuration.md`.
|
||||
|
||||
Validation run: `pnpm run test:fast` passed (config + core dist suite). New AniList queue tests run in core suite; token-store tests are auto-skipped in plain Node environments where Electron `safeStorage` is unavailable (still active when safeStorage exists).
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Completed AniList post-watch integration in the Electron app with secure token lifecycle, post-watch progress sync, and persistent retry/backoff queue with dead-letter behavior. Added user-facing control surfaces via CLI (`--anilist-status`, `--anilist-logout`, `--anilist-setup`, `--anilist-retry-queue`) and IPC (`anilist:get-status`, `anilist:clear-token`, `anilist:open-setup`, `anilist:get-queue-status`, `anilist:retry-now`), plus docs and test coverage; validated via `pnpm run test:fast`.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
|
||||
## Definition of Done
|
||||
<!-- DOD:BEGIN -->
|
||||
- [ ] #1 Core Anilist service module exists and is wired into application flow for post-watch updates.
|
||||
- [ ] #2 OAuth/token lifecycle is implemented with safe local persistence and revocation/logout behavior.
|
||||
- [ ] #3 A retry/backoff and dead-letter strategy for failed syncs is implemented.
|
||||
- [ ] #4 User-visible settings/docs explain how to connect/manage Anilist and what data is synced.
|
||||
- [ ] #5 At least smoke/integration coverage (or validated test plan) for mapping and sync flow is in place.
|
||||
- [x] #1 Core Anilist service module exists and is wired into application flow for post-watch updates.
|
||||
- [x] #2 OAuth/token lifecycle is implemented with safe local persistence and revocation/logout behavior.
|
||||
- [x] #3 A retry/backoff and dead-letter strategy for failed syncs is implemented.
|
||||
- [x] #4 User-visible settings/docs explain how to connect/manage Anilist and what data is synced.
|
||||
- [x] #5 At least smoke/integration coverage (or validated test plan) for mapping and sync flow is in place.
|
||||
<!-- DOD:END -->
|
||||
|
||||
@@ -427,6 +427,21 @@ Token + detection notes:
|
||||
- Detection quality is best when `guessit` is installed and available on `PATH`.
|
||||
- When `guessit` cannot parse or is missing, SubMiner falls back automatically to internal filename parsing.
|
||||
|
||||
AniList CLI commands:
|
||||
|
||||
- `--anilist-status`: print current AniList token resolution state and retry queue counters.
|
||||
- `--anilist-logout`: clear stored AniList token from local persisted state.
|
||||
- `--anilist-setup`: open AniList setup/auth flow helper window.
|
||||
- `--anilist-retry-queue`: process one ready retry queue item immediately.
|
||||
|
||||
AniList IPC channels:
|
||||
|
||||
- `anilist:get-status`: return token status + retry queue state snapshot.
|
||||
- `anilist:clear-token`: clear stored AniList token and reset token status state.
|
||||
- `anilist:open-setup`: open AniList setup/auth flow helper window.
|
||||
- `anilist:get-queue-status`: return retry queue state snapshot.
|
||||
- `anilist:retry-now`: process one ready retry queue item immediately.
|
||||
|
||||
### Keybindings
|
||||
|
||||
Add a `keybindings` array to configure keyboard shortcuts that send commands to mpv:
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"docs:build": "VITE_EXTRA_EXTENSIONS=jsonc vitepress build docs",
|
||||
"docs:preview": "VITE_EXTRA_EXTENSIONS=jsonc vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort",
|
||||
"test:config:dist": "node --test dist/config/config.test.js",
|
||||
"test:core:dist": "node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/tokenizer.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js",
|
||||
"test:core:dist": "node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/tokenizer.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js",
|
||||
"test:subtitle:dist": "echo \"Subtitle tests are currently not configured\"",
|
||||
"test": "pnpm run test:config && pnpm run test:core",
|
||||
"test:config": "pnpm run build && pnpm run test:config:dist",
|
||||
|
||||
@@ -46,4 +46,14 @@ test("hasExplicitCommand and shouldStartApp preserve command intent", () => {
|
||||
assert.equal(refreshKnownWords.help, false);
|
||||
assert.equal(hasExplicitCommand(refreshKnownWords), true);
|
||||
assert.equal(shouldStartApp(refreshKnownWords), false);
|
||||
|
||||
const anilistStatus = parseArgs(["--anilist-status"]);
|
||||
assert.equal(anilistStatus.anilistStatus, true);
|
||||
assert.equal(hasExplicitCommand(anilistStatus), true);
|
||||
assert.equal(shouldStartApp(anilistStatus), false);
|
||||
|
||||
const anilistRetryQueue = parseArgs(["--anilist-retry-queue"]);
|
||||
assert.equal(anilistRetryQueue.anilistRetryQueue, true);
|
||||
assert.equal(hasExplicitCommand(anilistRetryQueue), true);
|
||||
assert.equal(shouldStartApp(anilistRetryQueue), false);
|
||||
});
|
||||
|
||||
@@ -22,6 +22,10 @@ export interface CliArgs {
|
||||
triggerSubsync: boolean;
|
||||
markAudioCard: boolean;
|
||||
openRuntimeOptions: boolean;
|
||||
anilistStatus: boolean;
|
||||
anilistLogout: boolean;
|
||||
anilistSetup: boolean;
|
||||
anilistRetryQueue: boolean;
|
||||
texthooker: boolean;
|
||||
help: boolean;
|
||||
autoStartOverlay: boolean;
|
||||
@@ -62,6 +66,10 @@ export function parseArgs(argv: string[]): CliArgs {
|
||||
triggerSubsync: false,
|
||||
markAudioCard: false,
|
||||
openRuntimeOptions: false,
|
||||
anilistStatus: false,
|
||||
anilistLogout: false,
|
||||
anilistSetup: false,
|
||||
anilistRetryQueue: false,
|
||||
texthooker: false,
|
||||
help: false,
|
||||
autoStartOverlay: false,
|
||||
@@ -109,6 +117,10 @@ export function parseArgs(argv: string[]): CliArgs {
|
||||
else if (arg === "--trigger-subsync") args.triggerSubsync = true;
|
||||
else if (arg === "--mark-audio-card") args.markAudioCard = true;
|
||||
else if (arg === "--open-runtime-options") args.openRuntimeOptions = true;
|
||||
else if (arg === "--anilist-status") args.anilistStatus = true;
|
||||
else if (arg === "--anilist-logout") args.anilistLogout = true;
|
||||
else if (arg === "--anilist-setup") args.anilistSetup = true;
|
||||
else if (arg === "--anilist-retry-queue") args.anilistRetryQueue = true;
|
||||
else if (arg === "--texthooker") args.texthooker = true;
|
||||
else if (arg === "--auto-start-overlay") args.autoStartOverlay = true;
|
||||
else if (arg === "--generate-config") args.generateConfig = true;
|
||||
@@ -190,6 +202,10 @@ export function hasExplicitCommand(args: CliArgs): boolean {
|
||||
args.triggerSubsync ||
|
||||
args.markAudioCard ||
|
||||
args.openRuntimeOptions ||
|
||||
args.anilistStatus ||
|
||||
args.anilistLogout ||
|
||||
args.anilistSetup ||
|
||||
args.anilistRetryQueue ||
|
||||
args.texthooker ||
|
||||
args.generateConfig ||
|
||||
args.help
|
||||
|
||||
@@ -18,4 +18,6 @@ test("printHelp includes configured texthooker port", () => {
|
||||
assert.match(output, /--help\s+Show this help/);
|
||||
assert.match(output, /default: 7777/);
|
||||
assert.match(output, /--refresh-known-words/);
|
||||
assert.match(output, /--anilist-status/);
|
||||
assert.match(output, /--anilist-retry-queue/);
|
||||
});
|
||||
|
||||
@@ -25,6 +25,10 @@ SubMiner CLI commands:
|
||||
--trigger-subsync Run subtitle sync
|
||||
--mark-audio-card Mark last card as audio card
|
||||
--open-runtime-options Open runtime options palette
|
||||
--anilist-status Show AniList token and retry queue status
|
||||
--anilist-logout Clear stored AniList token
|
||||
--anilist-setup Open AniList setup flow in app/browser
|
||||
--anilist-retry-queue Retry next ready AniList queue item now
|
||||
--auto-start-overlay Auto-hide mpv subtitles on connect (show overlay)
|
||||
--socket PATH Override MPV IPC socket/pipe path
|
||||
--backend BACKEND Override window tracker backend (auto, hyprland, sway, x11, macos)
|
||||
|
||||
162
src/core/services/anilist/anilist-token-store.test.ts
Normal file
162
src/core/services/anilist/anilist-token-store.test.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import { safeStorage } from "electron";
|
||||
|
||||
import { createAnilistTokenStore } from "./anilist-token-store";
|
||||
|
||||
function createTempTokenFile(): string {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "subminer-anilist-token-"));
|
||||
return path.join(dir, "token.json");
|
||||
}
|
||||
|
||||
function createLogger() {
|
||||
return {
|
||||
info: (_message: string) => {},
|
||||
warn: (_message: string) => {},
|
||||
error: (_message: string) => {},
|
||||
};
|
||||
}
|
||||
|
||||
type SafeStorageLike = {
|
||||
isEncryptionAvailable: () => boolean;
|
||||
encryptString: (value: string) => Buffer;
|
||||
decryptString: (value: Buffer) => string;
|
||||
};
|
||||
|
||||
const safeStorageApi = safeStorage as unknown as Partial<SafeStorageLike>;
|
||||
const hasSafeStorage =
|
||||
typeof safeStorageApi?.isEncryptionAvailable === "function" &&
|
||||
typeof safeStorageApi?.encryptString === "function" &&
|
||||
typeof safeStorageApi?.decryptString === "function";
|
||||
|
||||
const originalSafeStorage: SafeStorageLike | null = hasSafeStorage
|
||||
? {
|
||||
isEncryptionAvailable: safeStorageApi.isEncryptionAvailable as () => boolean,
|
||||
encryptString: safeStorageApi.encryptString as (value: string) => Buffer,
|
||||
decryptString: safeStorageApi.decryptString as (value: Buffer) => string,
|
||||
}
|
||||
: null;
|
||||
|
||||
function mockSafeStorage(encryptionAvailable: boolean): void {
|
||||
if (!hasSafeStorage) return;
|
||||
(
|
||||
safeStorage as unknown as {
|
||||
isEncryptionAvailable: typeof safeStorage.isEncryptionAvailable;
|
||||
encryptString: typeof safeStorage.encryptString;
|
||||
decryptString: typeof safeStorage.decryptString;
|
||||
}
|
||||
).isEncryptionAvailable = () => encryptionAvailable;
|
||||
(
|
||||
safeStorage as unknown as {
|
||||
encryptString: typeof safeStorage.encryptString;
|
||||
decryptString: typeof safeStorage.decryptString;
|
||||
}
|
||||
).encryptString = (value: string) => Buffer.from(`enc:${value}`, "utf-8");
|
||||
(
|
||||
safeStorage as unknown as {
|
||||
decryptString: typeof safeStorage.decryptString;
|
||||
}
|
||||
).decryptString = (value: Buffer) => {
|
||||
const raw = value.toString("utf-8");
|
||||
return raw.startsWith("enc:") ? raw.slice(4) : raw;
|
||||
};
|
||||
}
|
||||
|
||||
function restoreSafeStorage(): void {
|
||||
if (!hasSafeStorage || !originalSafeStorage) return;
|
||||
(
|
||||
safeStorage as unknown as {
|
||||
isEncryptionAvailable: typeof safeStorage.isEncryptionAvailable;
|
||||
encryptString: typeof safeStorage.encryptString;
|
||||
decryptString: typeof safeStorage.decryptString;
|
||||
}
|
||||
).isEncryptionAvailable = originalSafeStorage.isEncryptionAvailable;
|
||||
(
|
||||
safeStorage as unknown as {
|
||||
encryptString: typeof safeStorage.encryptString;
|
||||
decryptString: typeof safeStorage.decryptString;
|
||||
}
|
||||
).encryptString = originalSafeStorage.encryptString;
|
||||
(
|
||||
safeStorage as unknown as {
|
||||
decryptString: typeof safeStorage.decryptString;
|
||||
}
|
||||
).decryptString = originalSafeStorage.decryptString;
|
||||
}
|
||||
|
||||
test("anilist token store saves and loads encrypted token", { skip: !hasSafeStorage }, () => {
|
||||
mockSafeStorage(true);
|
||||
try {
|
||||
const filePath = createTempTokenFile();
|
||||
const store = createAnilistTokenStore(filePath, createLogger());
|
||||
store.saveToken(" demo-token ");
|
||||
|
||||
const payload = JSON.parse(fs.readFileSync(filePath, "utf-8")) as {
|
||||
encryptedToken?: string;
|
||||
plaintextToken?: string;
|
||||
};
|
||||
assert.equal(typeof payload.encryptedToken, "string");
|
||||
assert.equal(payload.plaintextToken, undefined);
|
||||
assert.equal(store.loadToken(), "demo-token");
|
||||
} finally {
|
||||
restoreSafeStorage();
|
||||
}
|
||||
});
|
||||
|
||||
test("anilist token store falls back to plaintext when encryption unavailable", { skip: !hasSafeStorage }, () => {
|
||||
mockSafeStorage(false);
|
||||
try {
|
||||
const filePath = createTempTokenFile();
|
||||
const store = createAnilistTokenStore(filePath, createLogger());
|
||||
store.saveToken("plain-token");
|
||||
|
||||
const payload = JSON.parse(fs.readFileSync(filePath, "utf-8")) as {
|
||||
plaintextToken?: string;
|
||||
};
|
||||
assert.equal(payload.plaintextToken, "plain-token");
|
||||
assert.equal(store.loadToken(), "plain-token");
|
||||
} finally {
|
||||
restoreSafeStorage();
|
||||
}
|
||||
});
|
||||
|
||||
test("anilist token store migrates legacy plaintext to encrypted", { skip: !hasSafeStorage }, () => {
|
||||
const filePath = createTempTokenFile();
|
||||
fs.writeFileSync(
|
||||
filePath,
|
||||
JSON.stringify({ plaintextToken: "legacy-token", updatedAt: Date.now() }),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
mockSafeStorage(true);
|
||||
try {
|
||||
const store = createAnilistTokenStore(filePath, createLogger());
|
||||
assert.equal(store.loadToken(), "legacy-token");
|
||||
|
||||
const payload = JSON.parse(fs.readFileSync(filePath, "utf-8")) as {
|
||||
encryptedToken?: string;
|
||||
plaintextToken?: string;
|
||||
};
|
||||
assert.equal(typeof payload.encryptedToken, "string");
|
||||
assert.equal(payload.plaintextToken, undefined);
|
||||
} finally {
|
||||
restoreSafeStorage();
|
||||
}
|
||||
});
|
||||
|
||||
test("anilist token store clears persisted token file", { skip: !hasSafeStorage }, () => {
|
||||
mockSafeStorage(true);
|
||||
try {
|
||||
const filePath = createTempTokenFile();
|
||||
const store = createAnilistTokenStore(filePath, createLogger());
|
||||
store.saveToken("to-clear");
|
||||
assert.equal(fs.existsSync(filePath), true);
|
||||
store.clearToken();
|
||||
assert.equal(fs.existsSync(filePath), false);
|
||||
} finally {
|
||||
restoreSafeStorage();
|
||||
}
|
||||
});
|
||||
93
src/core/services/anilist/anilist-update-queue.test.ts
Normal file
93
src/core/services/anilist/anilist-update-queue.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
|
||||
import { createAnilistUpdateQueue } from "./anilist-update-queue";
|
||||
|
||||
function createTempQueueFile(): string {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "subminer-anilist-queue-"));
|
||||
return path.join(dir, "queue.json");
|
||||
}
|
||||
|
||||
function createLogger() {
|
||||
const info: string[] = [];
|
||||
const warn: string[] = [];
|
||||
const error: string[] = [];
|
||||
return {
|
||||
info,
|
||||
warn,
|
||||
error,
|
||||
logger: {
|
||||
info: (message: string) => info.push(message),
|
||||
warn: (message: string) => warn.push(message),
|
||||
error: (message: string) => error.push(message),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("anilist update queue enqueues, snapshots, and dequeues success", () => {
|
||||
const queueFile = createTempQueueFile();
|
||||
const loggerState = createLogger();
|
||||
const queue = createAnilistUpdateQueue(queueFile, loggerState.logger);
|
||||
|
||||
queue.enqueue("k1", "Demo", 1);
|
||||
const snapshot = queue.getSnapshot(Number.MAX_SAFE_INTEGER);
|
||||
assert.deepEqual(snapshot, { pending: 1, ready: 1, deadLetter: 0 });
|
||||
assert.equal(queue.nextReady(Number.MAX_SAFE_INTEGER)?.key, "k1");
|
||||
|
||||
queue.markSuccess("k1");
|
||||
assert.deepEqual(queue.getSnapshot(Number.MAX_SAFE_INTEGER), {
|
||||
pending: 0,
|
||||
ready: 0,
|
||||
deadLetter: 0,
|
||||
});
|
||||
assert.ok(loggerState.info.some((message) => message.includes("Queued AniList retry")));
|
||||
});
|
||||
|
||||
test("anilist update queue applies retry backoff and dead-letter", () => {
|
||||
const queueFile = createTempQueueFile();
|
||||
const loggerState = createLogger();
|
||||
const queue = createAnilistUpdateQueue(queueFile, loggerState.logger);
|
||||
|
||||
const now = 1_700_000_000_000;
|
||||
queue.enqueue("k2", "Backoff Demo", 2);
|
||||
|
||||
queue.markFailure("k2", "fail-1", now);
|
||||
const firstRetry = queue.nextReady(now);
|
||||
assert.equal(firstRetry, null);
|
||||
|
||||
const pendingPayload = JSON.parse(fs.readFileSync(queueFile, "utf-8")) as {
|
||||
pending: Array<{ attemptCount: number; nextAttemptAt: number }>;
|
||||
};
|
||||
assert.equal(pendingPayload.pending[0]?.attemptCount, 1);
|
||||
assert.equal(pendingPayload.pending[0]?.nextAttemptAt, now + 30_000);
|
||||
|
||||
for (let attempt = 2; attempt <= 8; attempt += 1) {
|
||||
queue.markFailure("k2", `fail-${attempt}`, now);
|
||||
}
|
||||
|
||||
const snapshot = queue.getSnapshot(Number.MAX_SAFE_INTEGER);
|
||||
assert.deepEqual(snapshot, { pending: 0, ready: 0, deadLetter: 1 });
|
||||
assert.ok(
|
||||
loggerState.warn.some((message) =>
|
||||
message.includes("AniList retry moved to dead-letter queue."),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("anilist update queue persists and reloads from disk", () => {
|
||||
const queueFile = createTempQueueFile();
|
||||
const loggerState = createLogger();
|
||||
const queueA = createAnilistUpdateQueue(queueFile, loggerState.logger);
|
||||
queueA.enqueue("k3", "Persist Demo", 3);
|
||||
|
||||
const queueB = createAnilistUpdateQueue(queueFile, loggerState.logger);
|
||||
assert.deepEqual(queueB.getSnapshot(Number.MAX_SAFE_INTEGER), {
|
||||
pending: 1,
|
||||
ready: 1,
|
||||
deadLetter: 0,
|
||||
});
|
||||
assert.equal(queueB.nextReady(Number.MAX_SAFE_INTEGER)?.title, "Persist Demo");
|
||||
});
|
||||
@@ -28,6 +28,10 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
markAudioCard: false,
|
||||
refreshKnownWords: false,
|
||||
openRuntimeOptions: false,
|
||||
anilistStatus: false,
|
||||
anilistLogout: false,
|
||||
anilistSetup: false,
|
||||
anilistRetryQueue: false,
|
||||
texthooker: false,
|
||||
help: false,
|
||||
autoStartOverlay: false,
|
||||
@@ -125,6 +129,35 @@ function createDeps(overrides: Partial<CliCommandServiceDeps> = {}) {
|
||||
openRuntimeOptionsPalette: () => {
|
||||
calls.push("openRuntimeOptionsPalette");
|
||||
},
|
||||
getAnilistStatus: () => ({
|
||||
tokenStatus: "resolved",
|
||||
tokenSource: "stored",
|
||||
tokenMessage: null,
|
||||
tokenResolvedAt: 1,
|
||||
tokenErrorAt: null,
|
||||
queuePending: 2,
|
||||
queueReady: 1,
|
||||
queueDeadLetter: 0,
|
||||
queueLastAttemptAt: 2,
|
||||
queueLastError: null,
|
||||
}),
|
||||
clearAnilistToken: () => {
|
||||
calls.push("clearAnilistToken");
|
||||
},
|
||||
openAnilistSetup: () => {
|
||||
calls.push("openAnilistSetup");
|
||||
},
|
||||
getAnilistQueueStatus: () => ({
|
||||
pending: 2,
|
||||
ready: 1,
|
||||
deadLetter: 0,
|
||||
lastAttemptAt: null,
|
||||
lastError: null,
|
||||
}),
|
||||
retryAnilistQueue: async () => {
|
||||
calls.push("retryAnilistQueue");
|
||||
return { ok: true, message: "AniList retry processed." };
|
||||
},
|
||||
printHelp: () => {
|
||||
calls.push("printHelp");
|
||||
},
|
||||
@@ -281,6 +314,8 @@ test("handleCliCommand handles visibility and utility command dispatches", () =>
|
||||
},
|
||||
{ args: { toggleSecondarySub: true }, expected: "cycleSecondarySubMode" },
|
||||
{ args: { openRuntimeOptions: true }, expected: "openRuntimeOptionsPalette" },
|
||||
{ args: { anilistLogout: true }, expected: "clearAnilistToken" },
|
||||
{ args: { anilistSetup: true }, expected: "openAnilistSetup" },
|
||||
];
|
||||
|
||||
for (const entry of cases) {
|
||||
@@ -293,6 +328,21 @@ test("handleCliCommand handles visibility and utility command dispatches", () =>
|
||||
}
|
||||
});
|
||||
|
||||
test("handleCliCommand logs AniList status details", () => {
|
||||
const { deps, calls } = createDeps();
|
||||
handleCliCommand(makeArgs({ anilistStatus: true }), "initial", deps);
|
||||
assert.ok(calls.some((value) => value.startsWith("log:AniList token status:")));
|
||||
assert.ok(calls.some((value) => value.startsWith("log:AniList queue:")));
|
||||
});
|
||||
|
||||
test("handleCliCommand runs AniList retry command", async () => {
|
||||
const { deps, calls } = createDeps();
|
||||
handleCliCommand(makeArgs({ anilistRetryQueue: true }), "initial", deps);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.ok(calls.includes("retryAnilistQueue"));
|
||||
assert.ok(calls.includes("log:AniList retry processed."));
|
||||
});
|
||||
|
||||
test("handleCliCommand runs refresh-known-words command", () => {
|
||||
const { deps, calls } = createDeps();
|
||||
|
||||
|
||||
@@ -35,6 +35,28 @@ export interface CliCommandServiceDeps {
|
||||
triggerSubsyncFromConfig: () => Promise<void>;
|
||||
markLastCardAsAudioCard: () => Promise<void>;
|
||||
openRuntimeOptionsPalette: () => void;
|
||||
getAnilistStatus: () => {
|
||||
tokenStatus: "not_checked" | "resolved" | "error";
|
||||
tokenSource: "none" | "literal" | "stored";
|
||||
tokenMessage: string | null;
|
||||
tokenResolvedAt: number | null;
|
||||
tokenErrorAt: number | null;
|
||||
queuePending: number;
|
||||
queueReady: number;
|
||||
queueDeadLetter: number;
|
||||
queueLastAttemptAt: number | null;
|
||||
queueLastError: string | null;
|
||||
};
|
||||
clearAnilistToken: () => void;
|
||||
openAnilistSetup: () => void;
|
||||
getAnilistQueueStatus: () => {
|
||||
pending: number;
|
||||
ready: number;
|
||||
deadLetter: number;
|
||||
lastAttemptAt: number | null;
|
||||
lastError: string | null;
|
||||
};
|
||||
retryAnilistQueue: () => Promise<{ ok: boolean; message: string }>;
|
||||
printHelp: () => void;
|
||||
hasMainWindow: () => boolean;
|
||||
getMultiCopyTimeoutMs: () => number;
|
||||
@@ -97,6 +119,14 @@ interface UiCliRuntime {
|
||||
printHelp: () => void;
|
||||
}
|
||||
|
||||
interface AnilistCliRuntime {
|
||||
getStatus: CliCommandServiceDeps["getAnilistStatus"];
|
||||
clearToken: CliCommandServiceDeps["clearAnilistToken"];
|
||||
openSetup: CliCommandServiceDeps["openAnilistSetup"];
|
||||
getQueueStatus: CliCommandServiceDeps["getAnilistQueueStatus"];
|
||||
retryQueueNow: CliCommandServiceDeps["retryAnilistQueue"];
|
||||
}
|
||||
|
||||
interface AppCliRuntime {
|
||||
stop: () => void;
|
||||
hasMainWindow: () => boolean;
|
||||
@@ -107,6 +137,7 @@ export interface CliCommandDepsRuntimeOptions {
|
||||
texthooker: TexthookerCliRuntime;
|
||||
overlay: OverlayCliRuntime;
|
||||
mining: MiningCliRuntime;
|
||||
anilist: AnilistCliRuntime;
|
||||
ui: UiCliRuntime;
|
||||
app: AppCliRuntime;
|
||||
getMultiCopyTimeoutMs: () => number;
|
||||
@@ -167,6 +198,11 @@ export function createCliCommandDepsRuntime(
|
||||
triggerSubsyncFromConfig: options.mining.triggerSubsyncFromConfig,
|
||||
markLastCardAsAudioCard: options.mining.markLastCardAsAudioCard,
|
||||
openRuntimeOptionsPalette: options.ui.openRuntimeOptionsPalette,
|
||||
getAnilistStatus: options.anilist.getStatus,
|
||||
clearAnilistToken: options.anilist.clearToken,
|
||||
openAnilistSetup: options.anilist.openSetup,
|
||||
getAnilistQueueStatus: options.anilist.getQueueStatus,
|
||||
retryAnilistQueue: options.anilist.retryQueueNow,
|
||||
printHelp: options.ui.printHelp,
|
||||
hasMainWindow: options.app.hasMainWindow,
|
||||
getMultiCopyTimeoutMs: options.getMultiCopyTimeoutMs,
|
||||
@@ -177,6 +213,11 @@ export function createCliCommandDepsRuntime(
|
||||
};
|
||||
}
|
||||
|
||||
function formatTimestamp(value: number | null): string {
|
||||
if (!value) return "never";
|
||||
return new Date(value).toISOString();
|
||||
}
|
||||
|
||||
function runAsyncWithOsd(
|
||||
task: () => Promise<void>,
|
||||
deps: CliCommandServiceDeps,
|
||||
@@ -217,6 +258,10 @@ export function handleCliCommand(
|
||||
args.triggerSubsync ||
|
||||
args.markAudioCard ||
|
||||
args.openRuntimeOptions ||
|
||||
args.anilistStatus ||
|
||||
args.anilistLogout ||
|
||||
args.anilistSetup ||
|
||||
args.anilistRetryQueue ||
|
||||
args.texthooker ||
|
||||
args.help;
|
||||
const ignoreStartOnly = source === "second-instance" && args.start && !hasNonStartAction;
|
||||
@@ -331,6 +376,47 @@ export function handleCliCommand(
|
||||
);
|
||||
} else if (args.openRuntimeOptions) {
|
||||
deps.openRuntimeOptionsPalette();
|
||||
} else if (args.anilistStatus) {
|
||||
const status = deps.getAnilistStatus();
|
||||
deps.log(
|
||||
`AniList token status: ${status.tokenStatus} (source=${status.tokenSource})`,
|
||||
);
|
||||
if (status.tokenMessage) {
|
||||
deps.log(`AniList token message: ${status.tokenMessage}`);
|
||||
}
|
||||
deps.log(
|
||||
`AniList token timestamps: resolved=${formatTimestamp(status.tokenResolvedAt)}, error=${formatTimestamp(status.tokenErrorAt)}`,
|
||||
);
|
||||
deps.log(
|
||||
`AniList queue: pending=${status.queuePending}, ready=${status.queueReady}, deadLetter=${status.queueDeadLetter}`,
|
||||
);
|
||||
deps.log(
|
||||
`AniList queue timestamps: lastAttempt=${formatTimestamp(status.queueLastAttemptAt)}`,
|
||||
);
|
||||
if (status.queueLastError) {
|
||||
deps.warn(`AniList queue last error: ${status.queueLastError}`);
|
||||
}
|
||||
} else if (args.anilistLogout) {
|
||||
deps.clearAnilistToken();
|
||||
deps.log("Cleared stored AniList token.");
|
||||
} else if (args.anilistSetup) {
|
||||
deps.openAnilistSetup();
|
||||
deps.log("Opened AniList setup flow.");
|
||||
} else if (args.anilistRetryQueue) {
|
||||
const queueStatus = deps.getAnilistQueueStatus();
|
||||
deps.log(
|
||||
`AniList queue before retry: pending=${queueStatus.pending}, ready=${queueStatus.ready}, deadLetter=${queueStatus.deadLetter}`,
|
||||
);
|
||||
runAsyncWithOsd(
|
||||
async () => {
|
||||
const result = await deps.retryAnilistQueue();
|
||||
if (result.ok) deps.log(result.message);
|
||||
else deps.warn(result.message);
|
||||
},
|
||||
deps,
|
||||
"retryAnilistQueue",
|
||||
"AniList retry failed",
|
||||
);
|
||||
} else if (args.texthooker) {
|
||||
const texthookerPort = deps.getTexthookerPort();
|
||||
deps.ensureTexthookerRunning(texthookerPort);
|
||||
|
||||
60
src/core/services/ipc.test.ts
Normal file
60
src/core/services/ipc.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { createIpcDepsRuntime } from "./ipc";
|
||||
|
||||
test("createIpcDepsRuntime wires AniList handlers", async () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createIpcDepsRuntime({
|
||||
getInvisibleWindow: () => null,
|
||||
getMainWindow: () => null,
|
||||
getVisibleOverlayVisibility: () => false,
|
||||
getInvisibleOverlayVisibility: () => false,
|
||||
onOverlayModalClosed: () => {},
|
||||
openYomitanSettings: () => {},
|
||||
quitApp: () => {},
|
||||
toggleVisibleOverlay: () => {},
|
||||
tokenizeCurrentSubtitle: async () => null,
|
||||
getCurrentSubtitleAss: () => "",
|
||||
getMpvSubtitleRenderMetrics: () => null,
|
||||
getSubtitlePosition: () => null,
|
||||
getSubtitleStyle: () => null,
|
||||
saveSubtitlePosition: () => {},
|
||||
getMecabTokenizer: () => null,
|
||||
handleMpvCommand: () => {},
|
||||
getKeybindings: () => [],
|
||||
getConfiguredShortcuts: () => ({}),
|
||||
getSecondarySubMode: () => "hover",
|
||||
getMpvClient: () => null,
|
||||
focusMainWindow: () => {},
|
||||
runSubsyncManual: async () => ({}),
|
||||
getAnkiConnectStatus: () => false,
|
||||
getRuntimeOptions: () => ({}),
|
||||
setRuntimeOption: () => ({ ok: true }),
|
||||
cycleRuntimeOption: () => ({ ok: true }),
|
||||
reportOverlayContentBounds: () => {},
|
||||
getAnilistStatus: () => ({ tokenStatus: "resolved" }),
|
||||
clearAnilistToken: () => {
|
||||
calls.push("clearAnilistToken");
|
||||
},
|
||||
openAnilistSetup: () => {
|
||||
calls.push("openAnilistSetup");
|
||||
},
|
||||
getAnilistQueueStatus: () => ({ pending: 1, ready: 0, deadLetter: 0 }),
|
||||
retryAnilistQueueNow: async () => {
|
||||
calls.push("retryAnilistQueueNow");
|
||||
return { ok: true, message: "done" };
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(deps.getAnilistStatus(), { tokenStatus: "resolved" });
|
||||
deps.clearAnilistToken();
|
||||
deps.openAnilistSetup();
|
||||
assert.deepEqual(deps.getAnilistQueueStatus(), {
|
||||
pending: 1,
|
||||
ready: 0,
|
||||
deadLetter: 0,
|
||||
});
|
||||
assert.deepEqual(await deps.retryAnilistQueueNow(), { ok: true, message: "done" });
|
||||
assert.deepEqual(calls, ["clearAnilistToken", "openAnilistSetup", "retryAnilistQueueNow"]);
|
||||
});
|
||||
@@ -31,6 +31,11 @@ export interface IpcServiceDeps {
|
||||
setRuntimeOption: (id: string, value: unknown) => unknown;
|
||||
cycleRuntimeOption: (id: string, direction: 1 | -1) => unknown;
|
||||
reportOverlayContentBounds: (payload: unknown) => void;
|
||||
getAnilistStatus: () => unknown;
|
||||
clearAnilistToken: () => void;
|
||||
openAnilistSetup: () => void;
|
||||
getAnilistQueueStatus: () => unknown;
|
||||
retryAnilistQueueNow: () => Promise<{ ok: boolean; message: string }>;
|
||||
}
|
||||
|
||||
interface WindowLike {
|
||||
@@ -82,6 +87,11 @@ export interface IpcDepsRuntimeOptions {
|
||||
setRuntimeOption: (id: string, value: unknown) => unknown;
|
||||
cycleRuntimeOption: (id: string, direction: 1 | -1) => unknown;
|
||||
reportOverlayContentBounds: (payload: unknown) => void;
|
||||
getAnilistStatus: () => unknown;
|
||||
clearAnilistToken: () => void;
|
||||
openAnilistSetup: () => void;
|
||||
getAnilistQueueStatus: () => unknown;
|
||||
retryAnilistQueueNow: () => Promise<{ ok: boolean; message: string }>;
|
||||
}
|
||||
|
||||
export function createIpcDepsRuntime(
|
||||
@@ -140,6 +150,11 @@ export function createIpcDepsRuntime(
|
||||
setRuntimeOption: options.setRuntimeOption,
|
||||
cycleRuntimeOption: options.cycleRuntimeOption,
|
||||
reportOverlayContentBounds: options.reportOverlayContentBounds,
|
||||
getAnilistStatus: options.getAnilistStatus,
|
||||
clearAnilistToken: options.clearAnilistToken,
|
||||
openAnilistSetup: options.openAnilistSetup,
|
||||
getAnilistQueueStatus: options.getAnilistQueueStatus,
|
||||
retryAnilistQueueNow: options.retryAnilistQueueNow,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -279,4 +294,26 @@ export function registerIpcHandlers(deps: IpcServiceDeps): void {
|
||||
ipcMain.on("overlay-content-bounds:report", (_event: IpcMainEvent, payload: unknown) => {
|
||||
deps.reportOverlayContentBounds(payload);
|
||||
});
|
||||
|
||||
ipcMain.handle("anilist:get-status", () => {
|
||||
return deps.getAnilistStatus();
|
||||
});
|
||||
|
||||
ipcMain.handle("anilist:clear-token", () => {
|
||||
deps.clearAnilistToken();
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
ipcMain.handle("anilist:open-setup", () => {
|
||||
deps.openAnilistSetup();
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
ipcMain.handle("anilist:get-queue-status", () => {
|
||||
return deps.getAnilistQueueStatus();
|
||||
});
|
||||
|
||||
ipcMain.handle("anilist:retry-now", async () => {
|
||||
return await deps.retryAnilistQueueNow();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
triggerSubsync: false,
|
||||
markAudioCard: false,
|
||||
openRuntimeOptions: false,
|
||||
anilistStatus: false,
|
||||
anilistLogout: false,
|
||||
anilistSetup: false,
|
||||
anilistRetryQueue: false,
|
||||
texthooker: false,
|
||||
help: false,
|
||||
autoStartOverlay: false,
|
||||
|
||||
60
src/main.ts
60
src/main.ts
@@ -776,6 +776,44 @@ function refreshAnilistRetryQueueState(): void {
|
||||
};
|
||||
}
|
||||
|
||||
function getAnilistStatusSnapshot() {
|
||||
return {
|
||||
tokenStatus: appState.anilistClientSecretState.status,
|
||||
tokenSource: appState.anilistClientSecretState.source,
|
||||
tokenMessage: appState.anilistClientSecretState.message,
|
||||
tokenResolvedAt: appState.anilistClientSecretState.resolvedAt,
|
||||
tokenErrorAt: appState.anilistClientSecretState.errorAt,
|
||||
queuePending: appState.anilistRetryQueueState.pending,
|
||||
queueReady: appState.anilistRetryQueueState.ready,
|
||||
queueDeadLetter: appState.anilistRetryQueueState.deadLetter,
|
||||
queueLastAttemptAt: appState.anilistRetryQueueState.lastAttemptAt,
|
||||
queueLastError: appState.anilistRetryQueueState.lastError,
|
||||
};
|
||||
}
|
||||
|
||||
function getAnilistQueueStatusSnapshot() {
|
||||
refreshAnilistRetryQueueState();
|
||||
return {
|
||||
pending: appState.anilistRetryQueueState.pending,
|
||||
ready: appState.anilistRetryQueueState.ready,
|
||||
deadLetter: appState.anilistRetryQueueState.deadLetter,
|
||||
lastAttemptAt: appState.anilistRetryQueueState.lastAttemptAt,
|
||||
lastError: appState.anilistRetryQueueState.lastError,
|
||||
};
|
||||
}
|
||||
|
||||
function clearAnilistTokenState(): void {
|
||||
anilistTokenStore.clearToken();
|
||||
anilistCachedAccessToken = null;
|
||||
setAnilistClientSecretState({
|
||||
status: "not_checked",
|
||||
source: "none",
|
||||
message: "stored token cleared",
|
||||
resolvedAt: null,
|
||||
errorAt: null,
|
||||
});
|
||||
}
|
||||
|
||||
function isAnilistTrackingEnabled(resolved: ResolvedConfig): boolean {
|
||||
return resolved.anilist.enabled;
|
||||
}
|
||||
@@ -1070,18 +1108,21 @@ function rememberAnilistAttemptedUpdateKey(key: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
async function processNextAnilistRetryUpdate(): Promise<void> {
|
||||
async function processNextAnilistRetryUpdate(): Promise<{
|
||||
ok: boolean;
|
||||
message: string;
|
||||
}> {
|
||||
const queued = anilistUpdateQueue.nextReady();
|
||||
refreshAnilistRetryQueueState();
|
||||
if (!queued) {
|
||||
return;
|
||||
return { ok: true, message: "AniList queue has no ready items." };
|
||||
}
|
||||
|
||||
appState.anilistRetryQueueState.lastAttemptAt = Date.now();
|
||||
const accessToken = await refreshAnilistClientSecretState();
|
||||
if (!accessToken) {
|
||||
appState.anilistRetryQueueState.lastError = "AniList token unavailable for queued retry.";
|
||||
return;
|
||||
return { ok: false, message: appState.anilistRetryQueueState.lastError };
|
||||
}
|
||||
|
||||
const result = await updateAnilistPostWatchProgress(
|
||||
@@ -1095,12 +1136,13 @@ async function processNextAnilistRetryUpdate(): Promise<void> {
|
||||
appState.anilistRetryQueueState.lastError = null;
|
||||
refreshAnilistRetryQueueState();
|
||||
logger.info(`[AniList queue] ${result.message}`);
|
||||
return;
|
||||
return { ok: true, message: result.message };
|
||||
}
|
||||
|
||||
anilistUpdateQueue.markFailure(queued.key, result.message);
|
||||
appState.anilistRetryQueueState.lastError = result.message;
|
||||
refreshAnilistRetryQueueState();
|
||||
return { ok: false, message: result.message };
|
||||
}
|
||||
|
||||
async function maybeRunAnilistPostWatchUpdate(): Promise<void> {
|
||||
@@ -1441,6 +1483,11 @@ function handleCliCommand(
|
||||
triggerFieldGrouping: () => triggerFieldGrouping(),
|
||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||
markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
|
||||
getAnilistStatus: () => getAnilistStatusSnapshot(),
|
||||
clearAnilistToken: () => clearAnilistTokenState(),
|
||||
openAnilistSetup: () => openAnilistSetupWindow(),
|
||||
getAnilistQueueStatus: () => getAnilistQueueStatusSnapshot(),
|
||||
retryAnilistQueueNow: () => processNextAnilistRetryUpdate(),
|
||||
openYomitanSettings: () => openYomitanSettings(),
|
||||
cycleSecondarySubMode: () => cycleSecondarySubMode(),
|
||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||
@@ -2115,6 +2162,11 @@ registerIpcRuntimeServices({
|
||||
reportOverlayContentBounds: (payload: unknown) => {
|
||||
overlayContentMeasurementStore.report(payload);
|
||||
},
|
||||
getAnilistStatus: () => getAnilistStatusSnapshot(),
|
||||
clearAnilistToken: () => clearAnilistTokenState(),
|
||||
openAnilistSetup: () => openAnilistSetupWindow(),
|
||||
getAnilistQueueStatus: () => getAnilistQueueStatusSnapshot(),
|
||||
retryAnilistQueueNow: () => processNextAnilistRetryUpdate(),
|
||||
},
|
||||
ankiJimakuDeps: createAnkiJimakuIpcRuntimeServiceDeps({
|
||||
patchAnkiConnectEnabled: (enabled: boolean) => {
|
||||
|
||||
@@ -26,6 +26,11 @@ export interface CliCommandRuntimeServiceContext {
|
||||
triggerFieldGrouping: () => Promise<void>;
|
||||
triggerSubsyncFromConfig: () => Promise<void>;
|
||||
markLastCardAsAudioCard: () => Promise<void>;
|
||||
getAnilistStatus: CliCommandRuntimeServiceDepsParams["anilist"]["getStatus"];
|
||||
clearAnilistToken: CliCommandRuntimeServiceDepsParams["anilist"]["clearToken"];
|
||||
openAnilistSetup: CliCommandRuntimeServiceDepsParams["anilist"]["openSetup"];
|
||||
getAnilistQueueStatus: CliCommandRuntimeServiceDepsParams["anilist"]["getQueueStatus"];
|
||||
retryAnilistQueueNow: CliCommandRuntimeServiceDepsParams["anilist"]["retryQueueNow"];
|
||||
openYomitanSettings: () => void;
|
||||
cycleSecondarySubMode: () => void;
|
||||
openRuntimeOptionsPalette: () => void;
|
||||
@@ -71,13 +76,20 @@ function createCliCommandDepsFromContext(
|
||||
mining: {
|
||||
copyCurrentSubtitle: context.copyCurrentSubtitle,
|
||||
startPendingMultiCopy: context.startPendingMultiCopy,
|
||||
mineSentenceCard: context.mineSentenceCard,
|
||||
startPendingMineSentenceMultiple: context.startPendingMineSentenceMultiple,
|
||||
updateLastCardFromClipboard: context.updateLastCardFromClipboard,
|
||||
refreshKnownWords: context.refreshKnownWordCache,
|
||||
triggerFieldGrouping: context.triggerFieldGrouping,
|
||||
triggerSubsyncFromConfig: context.triggerSubsyncFromConfig,
|
||||
markLastCardAsAudioCard: context.markLastCardAsAudioCard,
|
||||
mineSentenceCard: context.mineSentenceCard,
|
||||
startPendingMineSentenceMultiple: context.startPendingMineSentenceMultiple,
|
||||
updateLastCardFromClipboard: context.updateLastCardFromClipboard,
|
||||
refreshKnownWords: context.refreshKnownWordCache,
|
||||
triggerFieldGrouping: context.triggerFieldGrouping,
|
||||
triggerSubsyncFromConfig: context.triggerSubsyncFromConfig,
|
||||
markLastCardAsAudioCard: context.markLastCardAsAudioCard,
|
||||
},
|
||||
anilist: {
|
||||
getStatus: context.getAnilistStatus,
|
||||
clearToken: context.clearAnilistToken,
|
||||
openSetup: context.openAnilistSetup,
|
||||
getQueueStatus: context.getAnilistQueueStatus,
|
||||
retryQueueNow: context.retryAnilistQueueNow,
|
||||
},
|
||||
ui: {
|
||||
openYomitanSettings: context.openYomitanSettings,
|
||||
|
||||
@@ -90,6 +90,11 @@ export interface MainIpcRuntimeServiceDepsParams {
|
||||
setRuntimeOption: IpcDepsRuntimeOptions["setRuntimeOption"];
|
||||
cycleRuntimeOption: IpcDepsRuntimeOptions["cycleRuntimeOption"];
|
||||
reportOverlayContentBounds: IpcDepsRuntimeOptions["reportOverlayContentBounds"];
|
||||
getAnilistStatus: IpcDepsRuntimeOptions["getAnilistStatus"];
|
||||
clearAnilistToken: IpcDepsRuntimeOptions["clearAnilistToken"];
|
||||
openAnilistSetup: IpcDepsRuntimeOptions["openAnilistSetup"];
|
||||
getAnilistQueueStatus: IpcDepsRuntimeOptions["getAnilistQueueStatus"];
|
||||
retryAnilistQueueNow: IpcDepsRuntimeOptions["retryAnilistQueueNow"];
|
||||
}
|
||||
|
||||
export interface AnkiJimakuIpcRuntimeServiceDepsParams {
|
||||
@@ -154,6 +159,13 @@ export interface CliCommandRuntimeServiceDepsParams {
|
||||
markLastCardAsAudioCard:
|
||||
CliCommandDepsRuntimeOptions["mining"]["markLastCardAsAudioCard"];
|
||||
};
|
||||
anilist: {
|
||||
getStatus: CliCommandDepsRuntimeOptions["anilist"]["getStatus"];
|
||||
clearToken: CliCommandDepsRuntimeOptions["anilist"]["clearToken"];
|
||||
openSetup: CliCommandDepsRuntimeOptions["anilist"]["openSetup"];
|
||||
getQueueStatus: CliCommandDepsRuntimeOptions["anilist"]["getQueueStatus"];
|
||||
retryQueueNow: CliCommandDepsRuntimeOptions["anilist"]["retryQueueNow"];
|
||||
};
|
||||
ui: {
|
||||
openYomitanSettings: CliCommandDepsRuntimeOptions["ui"]["openYomitanSettings"];
|
||||
cycleSecondarySubMode: CliCommandDepsRuntimeOptions["ui"]["cycleSecondarySubMode"];
|
||||
@@ -216,6 +228,11 @@ export function createMainIpcRuntimeServiceDeps(
|
||||
setRuntimeOption: params.setRuntimeOption,
|
||||
cycleRuntimeOption: params.cycleRuntimeOption,
|
||||
reportOverlayContentBounds: params.reportOverlayContentBounds,
|
||||
getAnilistStatus: params.getAnilistStatus,
|
||||
clearAnilistToken: params.clearAnilistToken,
|
||||
openAnilistSetup: params.openAnilistSetup,
|
||||
getAnilistQueueStatus: params.getAnilistQueueStatus,
|
||||
retryAnilistQueueNow: params.retryAnilistQueueNow,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -283,6 +300,13 @@ export function createCliCommandRuntimeServiceDeps(
|
||||
triggerSubsyncFromConfig: params.mining.triggerSubsyncFromConfig,
|
||||
markLastCardAsAudioCard: params.mining.markLastCardAsAudioCard,
|
||||
},
|
||||
anilist: {
|
||||
getStatus: params.anilist.getStatus,
|
||||
clearToken: params.anilist.clearToken,
|
||||
openSetup: params.anilist.openSetup,
|
||||
getQueueStatus: params.anilist.getQueueStatus,
|
||||
retryQueueNow: params.anilist.retryQueueNow,
|
||||
},
|
||||
ui: {
|
||||
openYomitanSettings: params.ui.openYomitanSettings,
|
||||
cycleSecondarySubMode: params.ui.cycleSecondarySubMode,
|
||||
|
||||
Reference in New Issue
Block a user