mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
feat(anilist): add CLI and IPC management controls
This commit is contained in:
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,
|
||||
|
||||
Reference in New Issue
Block a user