mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-30 07:21:32 -07:00
refactor(tsukihime): swap Animetosho backend for TsukiHime API (#165)
This commit is contained in:
@@ -40,7 +40,7 @@ const {
|
||||
const {
|
||||
ankiConnect,
|
||||
jimaku,
|
||||
animetosho,
|
||||
tsukihime,
|
||||
anilist,
|
||||
mpv,
|
||||
yomitan,
|
||||
@@ -73,7 +73,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
|
||||
subtitleSidebar,
|
||||
auto_start_overlay,
|
||||
jimaku,
|
||||
animetosho,
|
||||
tsukihime,
|
||||
anilist,
|
||||
mpv,
|
||||
yomitan,
|
||||
|
||||
@@ -98,7 +98,7 @@ export const CORE_DEFAULT_CONFIG: Pick<
|
||||
openCharacterDictionaryManager: 'CommandOrControl+D',
|
||||
openRuntimeOptions: 'CommandOrControl+Shift+O',
|
||||
openJimaku: 'Ctrl+Shift+J',
|
||||
openAnimetosho: 'Ctrl+Shift+T',
|
||||
openTsukihime: 'Ctrl+Shift+T',
|
||||
openSessionHelp: 'CommandOrControl+Slash',
|
||||
openControllerSelect: 'Alt+C',
|
||||
openControllerDebug: 'Alt+Shift+C',
|
||||
|
||||
@@ -5,7 +5,7 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
||||
ResolvedConfig,
|
||||
| 'ankiConnect'
|
||||
| 'jimaku'
|
||||
| 'animetosho'
|
||||
| 'tsukihime'
|
||||
| 'anilist'
|
||||
| 'mpv'
|
||||
| 'yomitan'
|
||||
@@ -97,8 +97,8 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
||||
languagePreference: 'ja',
|
||||
maxEntryResults: 10,
|
||||
},
|
||||
animetosho: {
|
||||
apiBaseUrl: 'https://feed.animetosho.org',
|
||||
tsukihime: {
|
||||
apiBaseUrl: 'https://api.tsukihime.org/v1',
|
||||
maxSearchResults: 10,
|
||||
},
|
||||
mpv: {
|
||||
|
||||
@@ -616,11 +616,11 @@ export function buildCoreConfigOptionRegistry(
|
||||
description: 'Accelerator that opens the Jimaku subtitle search modal.',
|
||||
},
|
||||
{
|
||||
path: 'shortcuts.openAnimetosho',
|
||||
path: 'shortcuts.openTsukihime',
|
||||
kind: 'string',
|
||||
defaultValue: defaultConfig.shortcuts.openAnimetosho,
|
||||
defaultValue: defaultConfig.shortcuts.openTsukihime,
|
||||
description:
|
||||
'Accelerator that opens the Animetosho subtitle search modal (English/Japanese tabs).',
|
||||
'Accelerator that opens the TsukiHime subtitle search modal (configured secondary/Japanese primary tabs).',
|
||||
},
|
||||
{
|
||||
path: 'shortcuts.openSessionHelp',
|
||||
|
||||
@@ -401,16 +401,17 @@ export function buildIntegrationConfigOptionRegistry(
|
||||
description: 'Maximum Jimaku search results returned.',
|
||||
},
|
||||
{
|
||||
path: 'animetosho.apiBaseUrl',
|
||||
path: 'tsukihime.apiBaseUrl',
|
||||
kind: 'string',
|
||||
defaultValue: defaultConfig.animetosho.apiBaseUrl,
|
||||
description: 'Base URL of the Animetosho JSON feed API. No API key required.',
|
||||
defaultValue: defaultConfig.tsukihime.apiBaseUrl,
|
||||
description:
|
||||
'Base URL of the TsukiHime API (Animetosho successor). No API key required.',
|
||||
},
|
||||
{
|
||||
path: 'animetosho.maxSearchResults',
|
||||
path: 'tsukihime.maxSearchResults',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.animetosho.maxSearchResults,
|
||||
description: 'Maximum Animetosho search results returned.',
|
||||
defaultValue: defaultConfig.tsukihime.maxSearchResults,
|
||||
description: 'Maximum TsukiHime search results returned.',
|
||||
},
|
||||
{
|
||||
path: 'anilist.enabled',
|
||||
|
||||
@@ -53,7 +53,9 @@ export const SPECIAL_COMMANDS = {
|
||||
SUBSYNC_TRIGGER: '__subsync-trigger',
|
||||
RUNTIME_OPTIONS_OPEN: '__runtime-options-open',
|
||||
JIMAKU_OPEN: '__jimaku-open',
|
||||
/** @deprecated Use TSUKIHIME_OPEN. */
|
||||
ANIMETOSHO_OPEN: '__animetosho-open',
|
||||
TSUKIHIME_OPEN: '__tsukihime-open',
|
||||
RUNTIME_OPTION_CYCLE_PREFIX: '__runtime-option-cycle:',
|
||||
REPLAY_SUBTITLE: '__replay-subtitle',
|
||||
PLAY_NEXT_SUBTITLE: '__play-next-subtitle',
|
||||
|
||||
@@ -148,12 +148,12 @@ const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
||||
key: 'jimaku',
|
||||
},
|
||||
{
|
||||
title: 'Animetosho',
|
||||
title: 'TsukiHime',
|
||||
description: [
|
||||
'Animetosho subtitle search configuration (English and Japanese). No API key required.',
|
||||
'TsukiHime subtitle search configuration for Japanese primary and configured secondary subtitles. No API key required.',
|
||||
],
|
||||
notes: ['Hot-reload: Animetosho changes apply to the next Animetosho request.'],
|
||||
key: 'animetosho',
|
||||
notes: ['Hot-reload: TsukiHime changes apply to the next TsukiHime request.'],
|
||||
key: 'tsukihime',
|
||||
},
|
||||
{
|
||||
title: 'YouTube Playback Settings',
|
||||
|
||||
@@ -236,7 +236,7 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
|
||||
'openCharacterDictionaryManager',
|
||||
'openRuntimeOptions',
|
||||
'openJimaku',
|
||||
'openAnimetosho',
|
||||
'openTsukihime',
|
||||
'openSessionHelp',
|
||||
'openControllerSelect',
|
||||
'openControllerDebug',
|
||||
@@ -254,6 +254,20 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
|
||||
}
|
||||
}
|
||||
|
||||
if (src.shortcuts.openTsukihime === undefined) {
|
||||
const legacyOpenTsukihime = src.shortcuts.openAnimetosho;
|
||||
if (typeof legacyOpenTsukihime === 'string' || legacyOpenTsukihime === null) {
|
||||
resolved.shortcuts.openTsukihime = legacyOpenTsukihime;
|
||||
} else if (legacyOpenTsukihime !== undefined) {
|
||||
warn(
|
||||
'shortcuts.openAnimetosho',
|
||||
legacyOpenTsukihime,
|
||||
resolved.shortcuts.openTsukihime,
|
||||
'Expected string or null.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const timeout = asNumber(src.shortcuts.multiCopyTimeoutMs);
|
||||
if (timeout !== undefined && timeout > 0) {
|
||||
resolved.shortcuts.multiCopyTimeoutMs = Math.floor(timeout);
|
||||
|
||||
@@ -80,18 +80,27 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
||||
}
|
||||
}
|
||||
|
||||
if (isObject(src.animetosho)) {
|
||||
const apiBaseUrl = asString(src.animetosho.apiBaseUrl);
|
||||
if (apiBaseUrl !== undefined) resolved.animetosho.apiBaseUrl = apiBaseUrl;
|
||||
const currentTsukihimeSource = isObject(src.tsukihime) ? src.tsukihime : null;
|
||||
if (src.tsukihime !== undefined && !currentTsukihimeSource) {
|
||||
warn('tsukihime', src.tsukihime, resolved.tsukihime, 'Expected object.');
|
||||
}
|
||||
|
||||
const maxSearchResults = asNumber(src.animetosho.maxSearchResults);
|
||||
const legacyTsukihimeSource =
|
||||
src.tsukihime === undefined && isObject(src.animetosho) ? src.animetosho : null;
|
||||
const tsukihimeSource = currentTsukihimeSource ?? legacyTsukihimeSource;
|
||||
const tsukihimeSourcePath = currentTsukihimeSource ? 'tsukihime' : 'animetosho';
|
||||
if (tsukihimeSource) {
|
||||
const apiBaseUrl = asString(tsukihimeSource.apiBaseUrl);
|
||||
if (apiBaseUrl !== undefined) resolved.tsukihime.apiBaseUrl = apiBaseUrl;
|
||||
|
||||
const maxSearchResults = asNumber(tsukihimeSource.maxSearchResults);
|
||||
if (maxSearchResults !== undefined && Math.floor(maxSearchResults) > 0) {
|
||||
resolved.animetosho.maxSearchResults = Math.floor(maxSearchResults);
|
||||
} else if (src.animetosho.maxSearchResults !== undefined) {
|
||||
resolved.tsukihime.maxSearchResults = Math.floor(maxSearchResults);
|
||||
} else if (tsukihimeSource.maxSearchResults !== undefined) {
|
||||
warn(
|
||||
'animetosho.maxSearchResults',
|
||||
src.animetosho.maxSearchResults,
|
||||
resolved.animetosho.maxSearchResults,
|
||||
`${tsukihimeSourcePath}.maxSearchResults`,
|
||||
tsukihimeSource.maxSearchResults,
|
||||
resolved.tsukihime.maxSearchResults,
|
||||
'Expected positive number.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { asBoolean } from './shared';
|
||||
|
||||
export function applyTopLevelConfig(context: ResolveContext): void {
|
||||
const { src, resolved, warn } = context;
|
||||
const knownTopLevelKeys = new Set(Object.keys(resolved));
|
||||
const knownTopLevelKeys = new Set([...Object.keys(resolved), 'animetosho']);
|
||||
for (const key of Object.keys(src)) {
|
||||
if (!knownTopLevelKeys.has(key)) {
|
||||
warn(key, src[key], undefined, 'Unknown top-level config key; ignored.');
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { resolveConfig } from '../resolve';
|
||||
|
||||
test('resolveConfig maps legacy Animetosho settings to TsukiHime', () => {
|
||||
const { resolved, warnings } = resolveConfig({
|
||||
animetosho: {
|
||||
apiBaseUrl: 'https://legacy.example/v1',
|
||||
maxSearchResults: 23,
|
||||
},
|
||||
shortcuts: {
|
||||
openAnimetosho: 'Ctrl+Alt+T',
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(resolved.tsukihime.apiBaseUrl, 'https://legacy.example/v1');
|
||||
assert.equal(resolved.tsukihime.maxSearchResults, 23);
|
||||
assert.equal(resolved.shortcuts.openTsukihime, 'Ctrl+Alt+T');
|
||||
assert.deepEqual(warnings, []);
|
||||
});
|
||||
|
||||
test('resolveConfig gives current TsukiHime settings precedence over legacy aliases', () => {
|
||||
const { resolved, warnings } = resolveConfig({
|
||||
animetosho: {
|
||||
apiBaseUrl: 'https://legacy.example/v1',
|
||||
maxSearchResults: 23,
|
||||
},
|
||||
tsukihime: {
|
||||
apiBaseUrl: 'https://current.example/v1',
|
||||
maxSearchResults: 7,
|
||||
},
|
||||
shortcuts: {
|
||||
openAnimetosho: 'Ctrl+Alt+T',
|
||||
openTsukihime: null,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(resolved.tsukihime.apiBaseUrl, 'https://current.example/v1');
|
||||
assert.equal(resolved.tsukihime.maxSearchResults, 7);
|
||||
assert.equal(resolved.shortcuts.openTsukihime, null);
|
||||
assert.deepEqual(warnings, []);
|
||||
});
|
||||
|
||||
test('resolveConfig does not fall back when the current TsukiHime setting is invalid', () => {
|
||||
const { resolved, warnings } = resolveConfig({
|
||||
animetosho: {
|
||||
apiBaseUrl: 'https://legacy.example/v1',
|
||||
maxSearchResults: 23,
|
||||
},
|
||||
tsukihime: 'invalid' as never,
|
||||
});
|
||||
|
||||
assert.equal(resolved.tsukihime.apiBaseUrl, 'https://api.tsukihime.org/v1');
|
||||
assert.equal(resolved.tsukihime.maxSearchResults, 10);
|
||||
assert.deepEqual(warnings, [
|
||||
{
|
||||
path: 'tsukihime',
|
||||
value: 'invalid',
|
||||
fallback: resolved.tsukihime,
|
||||
message: 'Expected object.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -247,6 +247,8 @@ test('settings registry routes playback-related integrations into integrations',
|
||||
assert.equal(field('jimaku.apiBaseUrl').section, 'Jimaku');
|
||||
assert.equal(field('subsync.replace').category, 'integrations');
|
||||
assert.equal(field('subsync.replace').section, 'Subtitle Sync');
|
||||
assert.equal(field('tsukihime.apiBaseUrl').category, 'integrations');
|
||||
assert.equal(field('tsukihime.apiBaseUrl').section, 'TsukiHime');
|
||||
});
|
||||
|
||||
test('settings registry puts feature toggles first, then other toggles alphabetically', () => {
|
||||
|
||||
@@ -422,7 +422,7 @@ function categoryAndSection(path: string): { category: ConfigSettingsCategory; s
|
||||
if (path.startsWith('mpv.') || path.startsWith('youtube.')) {
|
||||
return { category: 'behavior', section: topSection(path) };
|
||||
}
|
||||
if (path.startsWith('jimaku.')) {
|
||||
if (path.startsWith('jimaku.') || path.startsWith('tsukihime.')) {
|
||||
return { category: 'integrations', section: topSection(path) };
|
||||
}
|
||||
if (path.startsWith('subsync.')) {
|
||||
@@ -486,6 +486,7 @@ function topSection(path: string): string {
|
||||
notifications: 'Notifications',
|
||||
subsync: 'Subtitle Sync',
|
||||
texthooker: 'Texthooker',
|
||||
tsukihime: 'TsukiHime',
|
||||
updates: 'Updates',
|
||||
websocket: 'WebSocket server',
|
||||
yomitan: 'Yomitan',
|
||||
@@ -594,7 +595,7 @@ function subsectionForPath(path: string): string | undefined {
|
||||
leaf === 'openCharacterDictionaryManager' ||
|
||||
leaf === 'openRuntimeOptions' ||
|
||||
leaf === 'openJimaku' ||
|
||||
leaf === 'openAnimetosho' ||
|
||||
leaf === 'openTsukihime' ||
|
||||
leaf === 'openSessionHelp' ||
|
||||
leaf === 'openControllerSelect' ||
|
||||
leaf === 'openControllerDebug'
|
||||
|
||||
Reference in New Issue
Block a user