mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-10-21 05:01:26 -07:00
refactor(hachidori): drop the Docker management upload for character dictionaries
Every supported host now takes dictionary ZIPs over the link, so remove the port 8780 upload, its hachidori.externalHostManagementUrl override and the hachidori config section. A linked host without linked-import-v1 now reports the update or manual-import fix.
This commit is contained in:
18 files changed
+65
-404
No files matched your search
@@ -23,7 +23,6 @@ export type {
|
||||
|
||||
const {
|
||||
dictionaryBackend,
|
||||
hachidori,
|
||||
subtitlePosition,
|
||||
keybindings,
|
||||
websocket,
|
||||
@@ -60,7 +59,6 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
|
||||
subtitleSelection: { enabled: false },
|
||||
subtitleGeneration: { ...DEFAULT_SUBTITLE_GENERATION_CONFIG },
|
||||
dictionaryBackend,
|
||||
hachidori,
|
||||
subtitlePosition,
|
||||
keybindings,
|
||||
websocket,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ResolvedConfig } from '../../types/config';
|
||||
export const CORE_DEFAULT_CONFIG: Pick<
|
||||
ResolvedConfig,
|
||||
| 'dictionaryBackend'
|
||||
| 'hachidori'
|
||||
| 'subtitlePosition'
|
||||
| 'keybindings'
|
||||
| 'websocket'
|
||||
@@ -21,7 +20,6 @@ export const CORE_DEFAULT_CONFIG: Pick<
|
||||
| 'auto_start_overlay'
|
||||
> = {
|
||||
dictionaryBackend: 'yomitan',
|
||||
hachidori: { externalHostManagementUrl: '' },
|
||||
subtitlePosition: { yPercent: 10 },
|
||||
keybindings: [],
|
||||
websocket: {
|
||||
|
||||
@@ -81,13 +81,6 @@ export function buildCoreConfigOptionRegistry(
|
||||
] as const;
|
||||
|
||||
return [
|
||||
{
|
||||
path: 'hachidori.externalHostManagementUrl',
|
||||
kind: 'string',
|
||||
defaultValue: defaultConfig.hachidori.externalHostManagementUrl,
|
||||
description:
|
||||
'Management URL override for character dictionary uploads to a linked Hachidori Docker host. Empty uses the linked host on the Docker default management port.',
|
||||
},
|
||||
{
|
||||
path: 'dictionaryBackend',
|
||||
kind: 'enum',
|
||||
|
||||
@@ -9,14 +9,6 @@ const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
||||
],
|
||||
key: 'dictionaryBackend',
|
||||
},
|
||||
{
|
||||
title: 'Hachidori External Dictionary Imports',
|
||||
description: [
|
||||
'Override the linked Docker host management URL only for a non-default port or a reverse proxy.',
|
||||
],
|
||||
notes: ['Used only while Hachidori is linked to an external host.'],
|
||||
key: 'hachidori',
|
||||
},
|
||||
{
|
||||
title: 'Subtitle Selection',
|
||||
description: ['Select primary and secondary mpv subtitle tracks from the overlay.'],
|
||||
|
||||
@@ -2,26 +2,10 @@ import { ResolveContext } from './context';
|
||||
import { applyControllerConfig } from './controller';
|
||||
import { isNotificationType, isOverlayNotificationPosition } from '../../types/notification';
|
||||
import { asBoolean, asNumber, asString, isObject } from './shared';
|
||||
import { parseHachidoriManagementUrl } from '../../shared/hachidori-sharing';
|
||||
|
||||
export function applyCoreDomainConfig(context: ResolveContext): void {
|
||||
const { src, resolved, warn } = context;
|
||||
|
||||
if (isObject(src.hachidori) && src.hachidori.externalHostManagementUrl !== undefined) {
|
||||
try {
|
||||
resolved.hachidori.externalHostManagementUrl = parseHachidoriManagementUrl(
|
||||
src.hachidori.externalHostManagementUrl,
|
||||
);
|
||||
} catch {
|
||||
warn(
|
||||
'hachidori.externalHostManagementUrl',
|
||||
src.hachidori.externalHostManagementUrl,
|
||||
resolved.hachidori.externalHostManagementUrl,
|
||||
'Expected an HTTP(S) origin or an empty string.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (src.dictionaryBackend === 'yomitan' || src.dictionaryBackend === 'hachidori') {
|
||||
resolved.dictionaryBackend = src.dictionaryBackend;
|
||||
} else if (src.dictionaryBackend !== undefined) {
|
||||
|
||||
@@ -28,24 +28,3 @@ test('unknown dictionary backend values warn and preserve the default', () => {
|
||||
assert.equal(warnings[0]?.path, 'dictionaryBackend');
|
||||
}
|
||||
});
|
||||
|
||||
test('Hachidori external import URL accepts HTTP origins and rejects invalid targets', () => {
|
||||
assert.equal(resolveConfig({}).resolved.hachidori.externalHostManagementUrl, '');
|
||||
const result = resolveConfig({
|
||||
hachidori: { externalHostManagementUrl: 'http://127.0.0.1:8780/' },
|
||||
});
|
||||
assert.equal(result.resolved.hachidori.externalHostManagementUrl, 'http://127.0.0.1:8780');
|
||||
assert.deepEqual(result.warnings, []);
|
||||
for (const value of [
|
||||
'file:///tmp/dict',
|
||||
'http://host/import',
|
||||
'http://user:password@host',
|
||||
true,
|
||||
]) {
|
||||
const { context, warnings } = createResolveContext({});
|
||||
context.src.hachidori = { externalHostManagementUrl: value };
|
||||
applyCoreDomainConfig(context);
|
||||
assert.equal(context.resolved.hachidori.externalHostManagementUrl, '');
|
||||
assert.equal(warnings[0]?.path, 'hachidori.externalHostManagementUrl');
|
||||
}
|
||||
});
|
||||
@@ -1,101 +1,28 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtemp, writeFile } from 'node:fs/promises';
|
||||
import { createServer } from 'node:http';
|
||||
import { once } from 'node:events';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
resolveHachidoriManagementUrl,
|
||||
uploadHachidoriDictionary,
|
||||
} from './hachidori-dictionary-import';
|
||||
import { importYomitanDictionaryFromZip } from './yomitan-parser-runtime';
|
||||
import { createDeps } from './yomitan-scan-test-harness';
|
||||
|
||||
test('linked Hachidori uploads replacement bytes, retries a busy host, and never invokes local import', async () => {
|
||||
const zipPath = path.join(await mkdtemp(path.join(os.tmpdir(), 'hachi-import-')), 'merged.zip');
|
||||
const archive = Buffer.from('PK-test-archive');
|
||||
await writeFile(zipPath, archive);
|
||||
let attempts = 0;
|
||||
const server = createServer(async (request, response) => {
|
||||
attempts += 1;
|
||||
assert.equal(request.url, '/import?name=merged.zip&replace=true');
|
||||
assert.equal(request.method, 'POST');
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of request) chunks.push(Buffer.from(chunk));
|
||||
assert.deepEqual(Buffer.concat(chunks), archive);
|
||||
response.writeHead(attempts === 1 ? 409 : 200, { 'Content-Type': 'application/json' });
|
||||
response.end(
|
||||
JSON.stringify(attempts === 1 ? { error: 'busy' } : { ok: true, report: { success: true } }),
|
||||
);
|
||||
});
|
||||
server.listen(0, '127.0.0.1');
|
||||
await once(server, 'listening');
|
||||
const address = server.address();
|
||||
assert.ok(address && typeof address === 'object');
|
||||
const managementUrl = `http://127.0.0.1:${address.port}`;
|
||||
let connected = true;
|
||||
const deps = {
|
||||
...createDeps(async (script) => {
|
||||
assert.ok(script.includes('hd_sharing_status'), 'must not invoke local ZIP automation');
|
||||
return {
|
||||
ok: true,
|
||||
sharing: {
|
||||
client: {
|
||||
linked: true,
|
||||
connected,
|
||||
address: 'ws://127.0.0.1:8771/link',
|
||||
host: { name: 'Hachidori Docker host', dictionaryCount: 8 },
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
getYomitanExt: () => ({
|
||||
id: 'hachi',
|
||||
name: 'Hachidori',
|
||||
version: '1',
|
||||
path: '',
|
||||
url: '',
|
||||
manifest: {},
|
||||
}),
|
||||
};
|
||||
try {
|
||||
assert.equal(
|
||||
await importYomitanDictionaryFromZip(zipPath, deps, { error: assert.fail }, managementUrl),
|
||||
true,
|
||||
);
|
||||
assert.equal(attempts, 2);
|
||||
const logger = { error: () => {} };
|
||||
connected = false;
|
||||
assert.equal(await importYomitanDictionaryFromZip(zipPath, deps, logger, managementUrl), false);
|
||||
assert.equal(attempts, 2);
|
||||
} finally {
|
||||
server.closeAllConnections();
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
|
||||
test('a linked host that accepts imports gets the ZIP over the link, not its management API', async () => {
|
||||
const zipPath = path.join(await mkdtemp(path.join(os.tmpdir(), 'hachi-import-')), 'merged.zip');
|
||||
await writeFile(zipPath, 'PK-linked');
|
||||
const linkedUploads: string[] = [];
|
||||
// Serves both the parser window (sharing status) and the settings window (import).
|
||||
// Hachidori deps whose sharing status reports `host`, recording every archive the
|
||||
// settings automation imports (locally or forwarded over the link).
|
||||
function hachidoriDeps(host: Record<string, unknown> | null) {
|
||||
const imported: string[] = [];
|
||||
const runScript = async (script: string): Promise<unknown> => {
|
||||
if (script.includes('hd_sharing_status')) {
|
||||
return {
|
||||
ok: true,
|
||||
sharing: {
|
||||
client: {
|
||||
linked: true,
|
||||
connected: true,
|
||||
address: 'ws://desktop:8771/link',
|
||||
host: { name: 'Helium', dictionaryCount: 8, capabilities: ['linked-import-v1'] },
|
||||
},
|
||||
client: host
|
||||
? { linked: true, connected: true, address: 'ws://desktop:8771/link', host }
|
||||
: { linked: false },
|
||||
},
|
||||
};
|
||||
}
|
||||
const archiveUrl = /importDictionaryArchiveUrl\(\s*("[^"]+")/.exec(script)?.[1];
|
||||
if (archiveUrl) linkedUploads.push(await (await fetch(JSON.parse(archiveUrl))).text());
|
||||
if (archiveUrl) imported.push(await (await fetch(JSON.parse(archiveUrl))).text());
|
||||
return true;
|
||||
};
|
||||
const settingsWindow = {
|
||||
@@ -103,110 +30,58 @@ test('a linked host that accepts imports gets the ZIP over the link, not its man
|
||||
destroy: () => undefined,
|
||||
webContents: { executeJavaScript: runScript },
|
||||
};
|
||||
const deps = {
|
||||
...createDeps(runScript, {
|
||||
createYomitanExtensionWindow: async () => settingsWindow,
|
||||
}),
|
||||
getYomitanExt: () => ({
|
||||
id: 'hachi',
|
||||
name: 'Hachidori',
|
||||
version: '1',
|
||||
path: '',
|
||||
url: '',
|
||||
manifest: {},
|
||||
}),
|
||||
return {
|
||||
imported,
|
||||
deps: {
|
||||
...createDeps(runScript, { createYomitanExtensionWindow: async () => settingsWindow }),
|
||||
getYomitanExt: () => ({
|
||||
id: 'hachi',
|
||||
name: 'Hachidori',
|
||||
version: '1',
|
||||
path: '',
|
||||
url: '',
|
||||
manifest: {},
|
||||
}),
|
||||
},
|
||||
};
|
||||
// Nothing listens here: reaching the management API would fail the import.
|
||||
assert.equal(
|
||||
await importYomitanDictionaryFromZip(
|
||||
zipPath,
|
||||
deps,
|
||||
{ error: assert.fail },
|
||||
'http://127.0.0.1:9',
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.deepEqual(linkedUploads, ['PK-linked']);
|
||||
});
|
||||
}
|
||||
|
||||
test('Hachidori upload requires a successful import report, not just HTTP success', async () => {
|
||||
async function writeArchive(): Promise<string> {
|
||||
const zipPath = path.join(await mkdtemp(path.join(os.tmpdir(), 'hachi-import-')), 'merged.zip');
|
||||
await writeFile(zipPath, 'bad archive');
|
||||
const server = createServer((_request, response) => {
|
||||
response.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
response.end(
|
||||
JSON.stringify({ ok: true, report: { success: false, error: 'Invalid archive' } }),
|
||||
await writeFile(zipPath, 'PK-test');
|
||||
return zipPath;
|
||||
}
|
||||
|
||||
for (const [name, host] of [
|
||||
['an unlinked Hachidori imports the ZIP itself', null],
|
||||
[
|
||||
'a linked host that accepts imports gets the ZIP over the link',
|
||||
{ name: 'Helium', dictionaryCount: 8, capabilities: ['linked-import-v1'] },
|
||||
],
|
||||
] as const) {
|
||||
test(name, async () => {
|
||||
const { deps, imported } = hachidoriDeps(host);
|
||||
assert.equal(
|
||||
await importYomitanDictionaryFromZip(await writeArchive(), deps, { error: assert.fail }),
|
||||
true,
|
||||
);
|
||||
assert.deepEqual(imported, ['PK-test']);
|
||||
});
|
||||
server.listen(0, '127.0.0.1');
|
||||
await once(server, 'listening');
|
||||
const address = server.address();
|
||||
assert.ok(address && typeof address === 'object');
|
||||
try {
|
||||
await assert.rejects(
|
||||
uploadHachidoriDictionary(zipPath, `http://127.0.0.1:${address.port}`),
|
||||
/Invalid archive/,
|
||||
);
|
||||
} finally {
|
||||
server.closeAllConnections();
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('management URL comes from the linked Docker host unless overridden', () => {
|
||||
const docker = { address: 'ws://pve-main:8771/link', name: 'Hachidori Docker host' };
|
||||
const warnings: string[] = [];
|
||||
const warn = (message: string) => warnings.push(message);
|
||||
test('a linked host without linked imports fails with the update or manual-import fix', async () => {
|
||||
const { deps, imported } = hachidoriDeps({ name: 'Chrome', dictionaryCount: 8 });
|
||||
const zipPath = await writeArchive();
|
||||
const errors: string[] = [];
|
||||
assert.equal(
|
||||
resolveHachidoriManagementUrl(docker, '', 'merged.zip', warn),
|
||||
'http://pve-main:8780',
|
||||
await importYomitanDictionaryFromZip(zipPath, deps, {
|
||||
error: (...parts: unknown[]) => errors.push(parts.join(' ')),
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
resolveHachidoriManagementUrl(docker, 'http://pve-main:9000', 'merged.zip', warn),
|
||||
'http://pve-main:9000',
|
||||
);
|
||||
assert.deepEqual(warnings, []);
|
||||
assert.equal(
|
||||
resolveHachidoriManagementUrl(
|
||||
{ address: 'ws://127.0.0.1:8771/link', name: 'Hachidori Docker host' },
|
||||
'http://localhost:8780',
|
||||
'merged.zip',
|
||||
warn,
|
||||
),
|
||||
'http://localhost:8780',
|
||||
);
|
||||
assert.deepEqual(warnings, []);
|
||||
assert.equal(
|
||||
resolveHachidoriManagementUrl(docker, 'http://127.0.0.1:8780', 'merged.zip', warn),
|
||||
'http://127.0.0.1:8780',
|
||||
);
|
||||
assert.match(warnings[0] ?? '', /linked to pve-main/);
|
||||
});
|
||||
|
||||
test('hosts without linked imports or an override explain the update or manual import', () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveHachidoriManagementUrl(
|
||||
{ address: 'ws://desktop:8771/link', name: 'Chrome' },
|
||||
'',
|
||||
'/dicts/merged.zip',
|
||||
),
|
||||
/Chrome at desktop cannot receive dictionary uploads\. Update its Hachidori to 0\.2\.3 or later, or import \/dicts\/merged\.zip/,
|
||||
);
|
||||
});
|
||||
|
||||
test('unreachable management API names the origin instead of a bare fetch failure', async () => {
|
||||
const zipPath = path.join(await mkdtemp(path.join(os.tmpdir(), 'hachi-import-')), 'merged.zip');
|
||||
await writeFile(zipPath, 'archive');
|
||||
const server = createServer();
|
||||
server.listen(0, '127.0.0.1');
|
||||
await once(server, 'listening');
|
||||
const address = server.address();
|
||||
assert.ok(address && typeof address === 'object');
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
const origin = `http://127.0.0.1:${address.port}`;
|
||||
await assert.rejects(
|
||||
uploadHachidoriDictionary(zipPath, origin),
|
||||
new RegExp(`Could not reach the Hachidori management API at ${origin}: `),
|
||||
assert.deepEqual(imported, []);
|
||||
assert.match(
|
||||
errors[0] ?? '',
|
||||
/Update its Hachidori to 0\.2\.3 or later, or import .*merged\.zip/,
|
||||
);
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { setTimeout as delay } from 'node:timers/promises';
|
||||
import type { HachidoriHostStatus } from '../../../shared/hachidori-sharing';
|
||||
|
||||
// hachidori-docker names itself this in the sharing hello. Hosts that advertise
|
||||
// linked imports take uploads over the link instead; this management API covers
|
||||
// hachidori-docker and is the only route to it.
|
||||
export const HACHIDORI_DOCKER_HOST_NAME = 'Hachidori Docker host';
|
||||
export const HACHIDORI_DOCKER_MANAGEMENT_PORT = 8780;
|
||||
|
||||
const LOOPBACK_HOSTNAMES = new Set(['127.0.0.1', 'localhost', '[::1]']);
|
||||
|
||||
function sameMachine(left: string, right: string): boolean {
|
||||
return left === right || (LOOPBACK_HOSTNAMES.has(left) && LOOPBACK_HOSTNAMES.has(right));
|
||||
}
|
||||
|
||||
// Picks the management origin that holds the linked host's dictionaries. The link
|
||||
// address names the machine; `override` (hachidori.externalHostManagementUrl, already
|
||||
// normalized to an origin) covers a non-default port or a reverse proxy.
|
||||
export function resolveHachidoriManagementUrl(
|
||||
host: Pick<Extract<HachidoriHostStatus, { kind: 'connected' }>, 'address' | 'name'>,
|
||||
override: string,
|
||||
zipPath: string,
|
||||
warn?: (message: string) => void,
|
||||
): string {
|
||||
const linkHostname = new URL(host.address).hostname;
|
||||
if (override) {
|
||||
const overrideHostname = new URL(override).hostname;
|
||||
if (!sameMachine(overrideHostname, linkHostname)) {
|
||||
warn?.(
|
||||
`hachidori.externalHostManagementUrl (${override}) points at ${overrideHostname}, but Hachidori is linked to ${linkHostname}; uploads may miss the linked host.`,
|
||||
);
|
||||
}
|
||||
return override;
|
||||
}
|
||||
if (host.name !== HACHIDORI_DOCKER_HOST_NAME) {
|
||||
throw new Error(
|
||||
`The linked ${host.name} at ${linkHostname} cannot receive dictionary uploads. Update its Hachidori to 0.2.3 or later, or import ${zipPath} from its Hachidori settings.`,
|
||||
);
|
||||
}
|
||||
return new URL(`http://${linkHostname}:${HACHIDORI_DOCKER_MANAGEMENT_PORT}`).origin;
|
||||
}
|
||||
|
||||
// Upload from the main process: extension blob URLs cannot cross the sharing link,
|
||||
// and the Docker management API deliberately rejects browser cross-origin writes.
|
||||
export async function uploadHachidoriDictionary(zipPath: string, origin: string): Promise<void> {
|
||||
const url = new URL('/import', origin);
|
||||
url.searchParams.set('name', path.basename(zipPath));
|
||||
url.searchParams.set('replace', 'true');
|
||||
const bytes = await readFile(zipPath);
|
||||
const signal = AbortSignal.timeout(300_000);
|
||||
for (;;) {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/zip' },
|
||||
body: bytes,
|
||||
signal,
|
||||
redirect: 'error',
|
||||
});
|
||||
} catch (error) {
|
||||
// undici reports every network failure as "fetch failed"; the cause says why.
|
||||
const cause = error instanceof Error && error.cause instanceof Error ? error.cause : error;
|
||||
const reason = cause instanceof Error ? cause.message : String(cause);
|
||||
throw new Error(`Could not reach the Hachidori management API at ${origin}: ${reason}`);
|
||||
}
|
||||
if (response.status === 409) {
|
||||
await response.body?.cancel();
|
||||
await delay(500, undefined, { signal });
|
||||
continue;
|
||||
}
|
||||
const result: unknown = await response.json();
|
||||
if (
|
||||
response.ok &&
|
||||
typeof result === 'object' &&
|
||||
result !== null &&
|
||||
'ok' in result &&
|
||||
result.ok === true &&
|
||||
'report' in result &&
|
||||
typeof result.report === 'object' &&
|
||||
result.report !== null &&
|
||||
'success' in result.report &&
|
||||
result.report.success === true
|
||||
)
|
||||
return;
|
||||
const detail =
|
||||
typeof result === 'object' &&
|
||||
result !== null &&
|
||||
'error' in result &&
|
||||
typeof result.error === 'string'
|
||||
? result.error
|
||||
: JSON.stringify(result);
|
||||
throw new Error(`Hachidori dictionary upload failed (${response.status}): ${detail}`);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
import type { BrowserWindow, Extension, Session } from 'electron';
|
||||
import type { AnkiConnectConfig } from '../../../types';
|
||||
import { buildHachidoriAnkiHints } from './hachidori-anki-settings';
|
||||
import {
|
||||
resolveHachidoriManagementUrl,
|
||||
uploadHachidoriDictionary,
|
||||
} from './hachidori-dictionary-import';
|
||||
import {
|
||||
buildHachidoriSharingScript,
|
||||
HACHIDORI_LINKED_IMPORT_CAPABILITY,
|
||||
@@ -1804,7 +1800,6 @@ export async function importYomitanDictionaryFromZip(
|
||||
zipPath: string,
|
||||
deps: YomitanParserRuntimeDeps,
|
||||
logger: LoggerLike,
|
||||
hachidoriManagementUrl = '',
|
||||
): Promise<boolean> {
|
||||
const normalizedZipPath = zipPath.trim();
|
||||
if (!normalizedZipPath || !fs.existsSync(normalizedZipPath)) {
|
||||
@@ -1818,25 +1813,15 @@ export async function importYomitanDictionaryFromZip(
|
||||
const host = await requestHachidoriSharing({ type: 'hd_sharing_status' }, deps, logger);
|
||||
if (host.kind === 'disconnected' || host.kind === 'unavailable')
|
||||
throw new Error(host.message);
|
||||
// Hosts with linked imports take the ZIP over the link like a local import,
|
||||
// below. Older hosts and hachidori-docker need their management API.
|
||||
// A linked host takes the ZIP over the link like a local import, below,
|
||||
// but only once it advertises linked imports (Hachidori 0.2.3+).
|
||||
if (
|
||||
host.kind === 'connected' &&
|
||||
!host.capabilities.includes(HACHIDORI_LINKED_IMPORT_CAPABILITY)
|
||||
) {
|
||||
const origin = resolveHachidoriManagementUrl(
|
||||
host,
|
||||
hachidoriManagementUrl,
|
||||
normalizedZipPath,
|
||||
logger.warn,
|
||||
throw new Error(
|
||||
`The linked ${host.name} cannot receive dictionary uploads. Update its Hachidori to 0.2.3 or later, or import ${normalizedZipPath} from its Hachidori settings.`,
|
||||
);
|
||||
await uploadHachidoriDictionary(normalizedZipPath, origin);
|
||||
const window = deps.getYomitanParserWindow();
|
||||
if (window) clearYomitanParserCachesForWindow(window);
|
||||
logger.info?.(
|
||||
`Uploaded character dictionary to Hachidori host: ${path.basename(normalizedZipPath)}`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
|
||||
+5
-10
@@ -2732,16 +2732,11 @@ const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRunt
|
||||
return false;
|
||||
}
|
||||
await ensureYomitanExtensionLoaded();
|
||||
return await importYomitanDictionaryFromZip(
|
||||
zipPath,
|
||||
getYomitanParserRuntimeDeps(),
|
||||
{
|
||||
error: (message, ...args) => logger.error(message, ...args),
|
||||
info: (message, ...args) => logger.info(message, ...args),
|
||||
warn: (message, ...args) => logger.warn(message, ...args),
|
||||
},
|
||||
configService.getConfig().hachidori.externalHostManagementUrl,
|
||||
);
|
||||
return await importYomitanDictionaryFromZip(zipPath, getYomitanParserRuntimeDeps(), {
|
||||
error: (message, ...args) => logger.error(message, ...args),
|
||||
info: (message, ...args) => logger.info(message, ...args),
|
||||
warn: (message, ...args) => logger.warn(message, ...args),
|
||||
});
|
||||
},
|
||||
dictionaryImportReplacesExisting: () =>
|
||||
getYomitanParserRuntimeDeps().getYomitanExt()?.name === 'Hachidori',
|
||||
|
||||
@@ -19,23 +19,6 @@ export type HachidoriSharingRequest =
|
||||
| { type: 'hd_sharing_client_link'; address: string }
|
||||
| { type: 'hd_sharing_client_unlink' };
|
||||
|
||||
export function parseHachidoriManagementUrl(value: unknown): string {
|
||||
if (typeof value !== 'string') throw new Error('Expected an HTTP(S) origin or an empty string.');
|
||||
if (!value.trim()) return '';
|
||||
const url = new URL(value.trim());
|
||||
if (
|
||||
!['http:', 'https:'].includes(url.protocol) ||
|
||||
url.username ||
|
||||
url.password ||
|
||||
url.pathname !== '/' ||
|
||||
url.search ||
|
||||
url.hash
|
||||
) {
|
||||
throw new Error('Expected an HTTP(S) origin without credentials, a path, query, or fragment.');
|
||||
}
|
||||
return url.origin;
|
||||
}
|
||||
|
||||
function object(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
@@ -145,7 +145,6 @@ export type DictionaryBackend = 'yomitan' | 'hachidori';
|
||||
|
||||
export interface Config {
|
||||
dictionaryBackend?: DictionaryBackend;
|
||||
hachidori?: { externalHostManagementUrl?: string };
|
||||
subtitlePosition?: SubtitlePosition;
|
||||
keybindings?: Keybinding[];
|
||||
websocket?: WebSocketConfig;
|
||||
@@ -189,7 +188,6 @@ export type RawConfig = Config;
|
||||
|
||||
export interface ResolvedConfig {
|
||||
dictionaryBackend: DictionaryBackend;
|
||||
hachidori: { externalHostManagementUrl: string };
|
||||
subtitlePosition: SubtitlePosition;
|
||||
keybindings: Keybinding[];
|
||||
websocket: Required<WebSocketConfig>;
|
||||
|
||||
Reference in new issue
Block a user