mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-29 19:21:33 -07:00
refactor(tsukihime): swap Animetosho backend for TsukiHime API (#165)
This commit is contained in:
@@ -55,10 +55,10 @@ test('anki/jimaku IPC handlers reject malformed invoke payloads', async () => {
|
||||
isRemoteMediaPath: () => false,
|
||||
downloadToFile: async () => ({ ok: true, path: '/tmp/sub.ass' }),
|
||||
onDownloadedSubtitle: () => {},
|
||||
searchAnimetoshoEntries: async () => ({ ok: true, data: [] }),
|
||||
listAnimetoshoFiles: async () => ({ ok: true, data: [] }),
|
||||
downloadAnimetoshoSubtitle: async () => ({ ok: true, path: '/tmp/sub.en.ass' }),
|
||||
getAnimetoshoSecondaryLanguages: () => ['en'],
|
||||
searchTsukihimeEntries: async () => ({ ok: true, data: [] }),
|
||||
listTsukihimeFiles: async () => ({ ok: true, data: [] }),
|
||||
downloadTsukihimeSubtitle: async () => ({ ok: true, path: '/tmp/sub.en.ass' }),
|
||||
getTsukihimeSecondaryLanguages: () => ['en'],
|
||||
onDownloadedSecondarySubtitle: () => {},
|
||||
},
|
||||
registrar,
|
||||
@@ -98,41 +98,41 @@ test('anki/jimaku IPC handlers reject malformed invoke payloads', async () => {
|
||||
error: { error: 'Invalid Jimaku download query payload', code: 400 },
|
||||
});
|
||||
|
||||
const animetoshoSearchHandler = handleHandlers.get(IPC_CHANNELS.request.animetoshoSearchEntries);
|
||||
assert.ok(animetoshoSearchHandler);
|
||||
const invalidAnimetoshoSearch = await animetoshoSearchHandler!({}, { query: 12 });
|
||||
assert.deepEqual(invalidAnimetoshoSearch, {
|
||||
const tsukihimeSearchHandler = handleHandlers.get(IPC_CHANNELS.request.tsukihimeSearchEntries);
|
||||
assert.ok(tsukihimeSearchHandler);
|
||||
const invalidTsukihimeSearch = await tsukihimeSearchHandler!({}, { query: 12 });
|
||||
assert.deepEqual(invalidTsukihimeSearch, {
|
||||
ok: false,
|
||||
error: { error: 'Invalid Animetosho search query payload', code: 400 },
|
||||
error: { error: 'Invalid TsukiHime search query payload', code: 400 },
|
||||
});
|
||||
|
||||
const animetoshoFilesHandler = handleHandlers.get(IPC_CHANNELS.request.animetoshoListFiles);
|
||||
assert.ok(animetoshoFilesHandler);
|
||||
const invalidAnimetoshoFiles = await animetoshoFilesHandler!({}, { entryId: 'x' });
|
||||
assert.deepEqual(invalidAnimetoshoFiles, {
|
||||
const tsukihimeFilesHandler = handleHandlers.get(IPC_CHANNELS.request.tsukihimeListFiles);
|
||||
assert.ok(tsukihimeFilesHandler);
|
||||
const invalidTsukihimeFiles = await tsukihimeFilesHandler!({}, { entryId: 'x' });
|
||||
assert.deepEqual(invalidTsukihimeFiles, {
|
||||
ok: false,
|
||||
error: { error: 'Invalid Animetosho files query payload', code: 400 },
|
||||
error: { error: 'Invalid TsukiHime files query payload', code: 400 },
|
||||
});
|
||||
|
||||
const animetoshoDownloadHandler = handleHandlers.get(IPC_CHANNELS.request.animetoshoDownloadFile);
|
||||
assert.ok(animetoshoDownloadHandler);
|
||||
const invalidAnimetoshoDownload = await animetoshoDownloadHandler!({}, { entryId: 1, url: '/x' });
|
||||
assert.deepEqual(invalidAnimetoshoDownload, {
|
||||
const tsukihimeDownloadHandler = handleHandlers.get(IPC_CHANNELS.request.tsukihimeDownloadFile);
|
||||
assert.ok(tsukihimeDownloadHandler);
|
||||
const invalidTsukihimeDownload = await tsukihimeDownloadHandler!({}, { entryId: 1, url: '/x' });
|
||||
assert.deepEqual(invalidTsukihimeDownload, {
|
||||
ok: false,
|
||||
error: { error: 'Invalid Animetosho download query payload', code: 400 },
|
||||
error: { error: 'Invalid TsukiHime download query payload', code: 400 },
|
||||
});
|
||||
|
||||
const foreignUrlDownload = await animetoshoDownloadHandler!(
|
||||
const foreignUrlDownload = await tsukihimeDownloadHandler!(
|
||||
{},
|
||||
{ entryId: 1, url: 'https://evil.example/attach/00000001/1.xz', name: 'sub.ass' },
|
||||
);
|
||||
assert.deepEqual(foreignUrlDownload, {
|
||||
ok: false,
|
||||
error: { error: 'Refusing to download subtitle from a non-Animetosho URL.', code: 400 },
|
||||
error: { error: 'Refusing to download subtitle from a non-TsukiHime URL.', code: 400 },
|
||||
});
|
||||
});
|
||||
|
||||
test('animetosho downloads route by language: secondary for eng, primary for jpn', async () => {
|
||||
test('tsukihime downloads always route Japanese as primary', async () => {
|
||||
const { registrar, handleHandlers } = createFakeRegistrar();
|
||||
const primaryLoads: string[] = [];
|
||||
const secondaryLoads: string[] = [];
|
||||
@@ -160,10 +160,10 @@ test('animetosho downloads route by language: secondary for eng, primary for jpn
|
||||
onDownloadedSubtitle: (path) => {
|
||||
primaryLoads.push(path);
|
||||
},
|
||||
searchAnimetoshoEntries: async () => ({ ok: true, data: [] }),
|
||||
listAnimetoshoFiles: async () => ({ ok: true, data: [] }),
|
||||
downloadAnimetoshoSubtitle: async (_url, destPath) => ({ ok: true, path: destPath }),
|
||||
getAnimetoshoSecondaryLanguages: () => ['en'],
|
||||
searchTsukihimeEntries: async () => ({ ok: true, data: [] }),
|
||||
listTsukihimeFiles: async () => ({ ok: true, data: [] }),
|
||||
downloadTsukihimeSubtitle: async (_url, destPath) => ({ ok: true, path: destPath }),
|
||||
getTsukihimeSecondaryLanguages: () => ['ja'],
|
||||
onDownloadedSecondarySubtitle: (path) => {
|
||||
secondaryLoads.push(path);
|
||||
},
|
||||
@@ -171,13 +171,13 @@ test('animetosho downloads route by language: secondary for eng, primary for jpn
|
||||
registrar,
|
||||
);
|
||||
|
||||
const downloadHandler = handleHandlers.get(IPC_CHANNELS.request.animetoshoDownloadFile)!;
|
||||
const downloadHandler = handleHandlers.get(IPC_CHANNELS.request.tsukihimeDownloadFile)!;
|
||||
|
||||
const engResult = (await downloadHandler!(
|
||||
{},
|
||||
{
|
||||
entryId: 1,
|
||||
url: 'https://animetosho.org/storage/attach/00000001/1.xz',
|
||||
url: 'https://storage.tsukihime.org/attach/00000001/1.xz',
|
||||
name: 'episode.eng.ass',
|
||||
lang: 'eng',
|
||||
},
|
||||
@@ -191,7 +191,7 @@ test('animetosho downloads route by language: secondary for eng, primary for jpn
|
||||
{},
|
||||
{
|
||||
entryId: 1,
|
||||
url: 'https://animetosho.org/storage/attach/00000002/2.xz',
|
||||
url: 'https://storage.tsukihime.org/attach/00000002/2.xz',
|
||||
name: 'episode.jpn.ass',
|
||||
lang: 'jpn',
|
||||
},
|
||||
@@ -232,10 +232,10 @@ test('anki/jimaku IPC command handlers ignore malformed payloads', () => {
|
||||
isRemoteMediaPath: () => false,
|
||||
downloadToFile: async () => ({ ok: true, path: '/tmp/sub.ass' }),
|
||||
onDownloadedSubtitle: () => {},
|
||||
searchAnimetoshoEntries: async () => ({ ok: true, data: [] }),
|
||||
listAnimetoshoFiles: async () => ({ ok: true, data: [] }),
|
||||
downloadAnimetoshoSubtitle: async () => ({ ok: true, path: '/tmp/sub.en.ass' }),
|
||||
getAnimetoshoSecondaryLanguages: () => ['en'],
|
||||
searchTsukihimeEntries: async () => ({ ok: true, data: [] }),
|
||||
listTsukihimeFiles: async () => ({ ok: true, data: [] }),
|
||||
downloadTsukihimeSubtitle: async () => ({ ok: true, path: '/tmp/sub.en.ass' }),
|
||||
getTsukihimeSecondaryLanguages: () => ['en'],
|
||||
onDownloadedSecondarySubtitle: () => {},
|
||||
},
|
||||
registrar,
|
||||
|
||||
@@ -4,12 +4,12 @@ import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { createLogger } from '../../logger';
|
||||
import {
|
||||
AnimetoshoApiResponse,
|
||||
AnimetoshoDownloadResult,
|
||||
AnimetoshoEntry,
|
||||
AnimetoshoFilesQuery,
|
||||
AnimetoshoSearchQuery,
|
||||
AnimetoshoSubtitleFile,
|
||||
TsukihimeApiResponse,
|
||||
TsukihimeDownloadResult,
|
||||
TsukihimeEntry,
|
||||
TsukihimeFilesQuery,
|
||||
TsukihimeSearchQuery,
|
||||
TsukihimeSubtitleFile,
|
||||
JimakuApiResponse,
|
||||
JimakuDownloadResult,
|
||||
JimakuEntry,
|
||||
@@ -23,9 +23,9 @@ import {
|
||||
} from '../../types';
|
||||
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
|
||||
import {
|
||||
parseAnimetoshoDownloadQuery,
|
||||
parseAnimetoshoFilesQuery,
|
||||
parseAnimetoshoSearchQuery,
|
||||
parseTsukihimeDownloadQuery,
|
||||
parseTsukihimeFilesQuery,
|
||||
parseTsukihimeSearchQuery,
|
||||
parseJimakuDownloadQuery,
|
||||
parseJimakuFilesQuery,
|
||||
parseJimakuSearchQuery,
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
parseKikuMergePreviewRequest,
|
||||
} from '../../shared/ipc/validators';
|
||||
import { buildJimakuSubtitleFilenameFromMediaPath } from './jimaku-download-path';
|
||||
import { animetoshoLangToFilenameSuffix, isAnimetoshoDownloadUrl } from '../../animetosho/utils';
|
||||
import { tsukihimeLangToFilenameSuffix, isTsukihimeDownloadUrl } from '../../tsukihime/utils';
|
||||
|
||||
const { ipcMain } = electron;
|
||||
|
||||
@@ -57,14 +57,14 @@ export interface AnkiJimakuIpcDeps {
|
||||
headers: Record<string, string>,
|
||||
) => Promise<JimakuDownloadResult>;
|
||||
onDownloadedSubtitle: (pathToSubtitle: string) => void;
|
||||
searchAnimetoshoEntries: (
|
||||
query: AnimetoshoSearchQuery,
|
||||
) => Promise<AnimetoshoApiResponse<AnimetoshoEntry[]>>;
|
||||
listAnimetoshoFiles: (
|
||||
query: AnimetoshoFilesQuery,
|
||||
) => Promise<AnimetoshoApiResponse<AnimetoshoSubtitleFile[]>>;
|
||||
downloadAnimetoshoSubtitle: (url: string, destPath: string) => Promise<AnimetoshoDownloadResult>;
|
||||
getAnimetoshoSecondaryLanguages: () => string[];
|
||||
searchTsukihimeEntries: (
|
||||
query: TsukihimeSearchQuery,
|
||||
) => Promise<TsukihimeApiResponse<TsukihimeEntry[]>>;
|
||||
listTsukihimeFiles: (
|
||||
query: TsukihimeFilesQuery,
|
||||
) => Promise<TsukihimeApiResponse<TsukihimeSubtitleFile[]>>;
|
||||
downloadTsukihimeSubtitle: (url: string, destPath: string) => Promise<TsukihimeDownloadResult>;
|
||||
getTsukihimeSecondaryLanguages: () => string[];
|
||||
onDownloadedSecondarySubtitle: (pathToSubtitle: string) => void | Promise<void>;
|
||||
}
|
||||
|
||||
@@ -206,50 +206,50 @@ export function registerAnkiJimakuIpcHandlers(
|
||||
},
|
||||
);
|
||||
|
||||
ipc.handle(IPC_CHANNELS.request.animetoshoGetSecondaryLanguages, (): string[] => {
|
||||
return deps.getAnimetoshoSecondaryLanguages();
|
||||
ipc.handle(IPC_CHANNELS.request.tsukihimeGetSecondaryLanguages, (): string[] => {
|
||||
return deps.getTsukihimeSecondaryLanguages();
|
||||
});
|
||||
|
||||
ipc.handle(
|
||||
IPC_CHANNELS.request.animetoshoSearchEntries,
|
||||
async (_event, query: unknown): Promise<AnimetoshoApiResponse<AnimetoshoEntry[]>> => {
|
||||
const parsedQuery = parseAnimetoshoSearchQuery(query);
|
||||
IPC_CHANNELS.request.tsukihimeSearchEntries,
|
||||
async (_event, query: unknown): Promise<TsukihimeApiResponse<TsukihimeEntry[]>> => {
|
||||
const parsedQuery = parseTsukihimeSearchQuery(query);
|
||||
if (!parsedQuery) {
|
||||
return {
|
||||
ok: false,
|
||||
error: { error: 'Invalid Animetosho search query payload', code: 400 },
|
||||
error: { error: 'Invalid TsukiHime search query payload', code: 400 },
|
||||
};
|
||||
}
|
||||
return deps.searchAnimetoshoEntries(parsedQuery);
|
||||
return deps.searchTsukihimeEntries(parsedQuery);
|
||||
},
|
||||
);
|
||||
|
||||
ipc.handle(
|
||||
IPC_CHANNELS.request.animetoshoListFiles,
|
||||
async (_event, query: unknown): Promise<AnimetoshoApiResponse<AnimetoshoSubtitleFile[]>> => {
|
||||
const parsedQuery = parseAnimetoshoFilesQuery(query);
|
||||
IPC_CHANNELS.request.tsukihimeListFiles,
|
||||
async (_event, query: unknown): Promise<TsukihimeApiResponse<TsukihimeSubtitleFile[]>> => {
|
||||
const parsedQuery = parseTsukihimeFilesQuery(query);
|
||||
if (!parsedQuery) {
|
||||
return { ok: false, error: { error: 'Invalid Animetosho files query payload', code: 400 } };
|
||||
return { ok: false, error: { error: 'Invalid TsukiHime files query payload', code: 400 } };
|
||||
}
|
||||
return deps.listAnimetoshoFiles(parsedQuery);
|
||||
return deps.listTsukihimeFiles(parsedQuery);
|
||||
},
|
||||
);
|
||||
|
||||
ipc.handle(
|
||||
IPC_CHANNELS.request.animetoshoDownloadFile,
|
||||
async (_event, query: unknown): Promise<AnimetoshoDownloadResult> => {
|
||||
const parsedQuery = parseAnimetoshoDownloadQuery(query);
|
||||
IPC_CHANNELS.request.tsukihimeDownloadFile,
|
||||
async (_event, query: unknown): Promise<TsukihimeDownloadResult> => {
|
||||
const parsedQuery = parseTsukihimeDownloadQuery(query);
|
||||
if (!parsedQuery) {
|
||||
return {
|
||||
ok: false,
|
||||
error: { error: 'Invalid Animetosho download query payload', code: 400 },
|
||||
error: { error: 'Invalid TsukiHime download query payload', code: 400 },
|
||||
};
|
||||
}
|
||||
|
||||
if (!isAnimetoshoDownloadUrl(parsedQuery.url)) {
|
||||
if (!isTsukihimeDownloadUrl(parsedQuery.url)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: { error: 'Refusing to download subtitle from a non-Animetosho URL.', code: 400 },
|
||||
error: { error: 'Refusing to download subtitle from a non-TsukiHime URL.', code: 400 },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -259,13 +259,13 @@ export function registerAnkiJimakuIpcHandlers(
|
||||
}
|
||||
|
||||
const mediaDir = deps.isRemoteMediaPath(currentMediaPath)
|
||||
? fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-animetosho-'))
|
||||
? fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-tsukihime-'))
|
||||
: path.dirname(path.resolve(currentMediaPath));
|
||||
const safeName = path.basename(parsedQuery.name);
|
||||
if (!safeName) {
|
||||
return { ok: false, error: { error: 'Invalid subtitle filename.' } };
|
||||
}
|
||||
const languageSuffix = animetoshoLangToFilenameSuffix(parsedQuery.lang);
|
||||
const languageSuffix = tsukihimeLangToFilenameSuffix(parsedQuery.lang);
|
||||
const subtitleFilename = buildJimakuSubtitleFilenameFromMediaPath(
|
||||
currentMediaPath,
|
||||
safeName,
|
||||
@@ -276,24 +276,24 @@ export function registerAnkiJimakuIpcHandlers(
|
||||
const baseName = ext ? subtitleFilename.slice(0, -ext.length) : subtitleFilename;
|
||||
let targetPath = path.join(mediaDir, subtitleFilename);
|
||||
if (fs.existsSync(targetPath)) {
|
||||
targetPath = path.join(mediaDir, `${baseName} (animetosho-${parsedQuery.entryId})${ext}`);
|
||||
targetPath = path.join(mediaDir, `${baseName} (tsukihime-${parsedQuery.entryId})${ext}`);
|
||||
let counter = 2;
|
||||
while (fs.existsSync(targetPath)) {
|
||||
targetPath = path.join(
|
||||
mediaDir,
|
||||
`${baseName} (animetosho-${parsedQuery.entryId}-${counter})${ext}`,
|
||||
`${baseName} (tsukihime-${parsedQuery.entryId}-${counter})${ext}`,
|
||||
);
|
||||
counter += 1;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[animetosho] download-file name="${parsedQuery.name}" entryId=${parsedQuery.entryId}`,
|
||||
`[tsukihime] download-file name="${parsedQuery.name}" entryId=${parsedQuery.entryId}`,
|
||||
);
|
||||
const result = await deps.downloadAnimetoshoSubtitle(parsedQuery.url, targetPath);
|
||||
const result = await deps.downloadTsukihimeSubtitle(parsedQuery.url, targetPath);
|
||||
|
||||
if (result.ok) {
|
||||
logger.info(`[animetosho] download-file saved to ${result.path}`);
|
||||
logger.info(`[tsukihime] download-file saved to ${result.path}`);
|
||||
// Japanese tracks take the primary slot; anything else loads as the
|
||||
// secondary subtitle so the Japanese primary stays in place.
|
||||
if (languageSuffix === 'ja') {
|
||||
@@ -303,7 +303,7 @@ export function registerAnkiJimakuIpcHandlers(
|
||||
}
|
||||
} else {
|
||||
logger.error(
|
||||
`[animetosho] download-file failed: ${result.error?.error ?? 'unknown error'}`,
|
||||
`[tsukihime] download-file failed: ${result.error?.error ?? 'unknown error'}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ interface RuntimeHarness {
|
||||
patches: boolean[];
|
||||
broadcasts: number;
|
||||
fetchCalls: Array<{ endpoint: string; query?: Record<string, unknown> }>;
|
||||
animetoshoFetchCalls: Array<{ endpoint: string; query?: Record<string, unknown> }>;
|
||||
tsukihimeFetchCalls: Array<{ endpoint: string; query?: Record<string, unknown> }>;
|
||||
sentCommands: Array<{ command: (string | number)[] }>;
|
||||
};
|
||||
}
|
||||
@@ -26,7 +26,7 @@ function createHarness(): RuntimeHarness {
|
||||
endpoint: string;
|
||||
query?: Record<string, unknown>;
|
||||
}>,
|
||||
animetoshoFetchCalls: [] as Array<{
|
||||
tsukihimeFetchCalls: [] as Array<{
|
||||
endpoint: string;
|
||||
query?: Record<string, unknown>;
|
||||
}>,
|
||||
@@ -37,17 +37,18 @@ function createHarness(): RuntimeHarness {
|
||||
patchAnkiConnectEnabled: (enabled) => {
|
||||
state.patches.push(enabled);
|
||||
},
|
||||
getResolvedConfig: () => ({ animetosho: { maxSearchResults: 2 } }),
|
||||
getResolvedConfig: () => ({ tsukihime: { maxSearchResults: 2 } }),
|
||||
getRuntimeOptionsManager: () => null,
|
||||
animetoshoFetchJson: async (endpoint, query) => {
|
||||
state.animetoshoFetchCalls.push({
|
||||
tsukihimeFetchJson: async (endpoint, query) => {
|
||||
state.tsukihimeFetchCalls.push({
|
||||
endpoint,
|
||||
query: query as Record<string, unknown>,
|
||||
});
|
||||
if ((query as Record<string, unknown>)?.show === 'torrent') {
|
||||
if (endpoint.startsWith('/torrents/')) {
|
||||
return {
|
||||
ok: true,
|
||||
data: {
|
||||
id: 606713,
|
||||
files: [
|
||||
{
|
||||
id: 9,
|
||||
@@ -55,9 +56,8 @@ function createHarness(): RuntimeHarness {
|
||||
attachments: [
|
||||
{
|
||||
id: 1955356,
|
||||
type: 'subtitle',
|
||||
info: { codec: 'ASS', lang: 'eng', name: 'English subs' },
|
||||
size: 33075,
|
||||
type: 1,
|
||||
info: { codec: 'ASS', lang: 'en', name: 'English subs' },
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -67,11 +67,13 @@ function createHarness(): RuntimeHarness {
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
data: [
|
||||
{ id: 1, title: 'release a' },
|
||||
{ id: 2, title: 'release b' },
|
||||
{ id: 3, title: 'release c' },
|
||||
] as never,
|
||||
data: {
|
||||
results: [
|
||||
{ id: 1, name: 'release a' },
|
||||
{ id: 2, name: 'release b' },
|
||||
{ id: 3, name: 'release c' },
|
||||
],
|
||||
} as never,
|
||||
};
|
||||
},
|
||||
getSubtitleTimingTracker: () => null,
|
||||
@@ -172,9 +174,9 @@ test('registerAnkiJimakuIpcRuntime provides full handler surface', () => {
|
||||
'isRemoteMediaPath',
|
||||
'downloadToFile',
|
||||
'onDownloadedSubtitle',
|
||||
'searchAnimetoshoEntries',
|
||||
'listAnimetoshoFiles',
|
||||
'downloadAnimetoshoSubtitle',
|
||||
'searchTsukihimeEntries',
|
||||
'listTsukihimeFiles',
|
||||
'downloadTsukihimeSubtitle',
|
||||
'onDownloadedSecondarySubtitle',
|
||||
];
|
||||
|
||||
@@ -365,14 +367,14 @@ test('onDownloadedSecondarySubtitle retries until mpv reports the new track', as
|
||||
});
|
||||
});
|
||||
|
||||
test('searchAnimetoshoEntries caps results using animetosho.maxSearchResults', async () => {
|
||||
test('searchTsukihimeEntries caps results using tsukihime.maxSearchResults', async () => {
|
||||
const { registered, state } = createHarness();
|
||||
|
||||
const searchResult = await registered.searchAnimetoshoEntries!({ query: 'frieren 28' });
|
||||
assert.deepEqual(state.animetoshoFetchCalls, [
|
||||
const searchResult = await registered.searchTsukihimeEntries!({ query: 'frieren 28' });
|
||||
assert.deepEqual(state.tsukihimeFetchCalls, [
|
||||
{
|
||||
endpoint: '/json',
|
||||
query: { q: 'frieren 28', qx: 1 },
|
||||
endpoint: '/search/torrents',
|
||||
query: { q: 'frieren 28', limit: 2 },
|
||||
},
|
||||
]);
|
||||
assert.equal((searchResult as { ok: boolean }).ok, true);
|
||||
@@ -384,20 +386,20 @@ test('searchAnimetoshoEntries caps results using animetosho.maxSearchResults', a
|
||||
);
|
||||
});
|
||||
|
||||
test('listAnimetoshoFiles extracts subtitle attachments from torrent detail', async () => {
|
||||
test('listTsukihimeFiles extracts subtitle attachments from torrent detail', async () => {
|
||||
const { registered, state } = createHarness();
|
||||
|
||||
const filesResult = await registered.listAnimetoshoFiles!({ entryId: 606713 });
|
||||
assert.deepEqual(state.animetoshoFetchCalls, [
|
||||
const filesResult = await registered.listTsukihimeFiles!({ entryId: 606713 });
|
||||
assert.deepEqual(state.tsukihimeFetchCalls, [
|
||||
{
|
||||
endpoint: '/json',
|
||||
query: { show: 'torrent', id: 606713 },
|
||||
endpoint: '/torrents/606713',
|
||||
query: {},
|
||||
},
|
||||
]);
|
||||
assert.equal((filesResult as { ok: boolean }).ok, true);
|
||||
const files = (filesResult as { data: Array<Record<string, unknown>> }).data;
|
||||
assert.equal(files.length, 1);
|
||||
assert.equal(files[0]!.attachmentId, 1955356);
|
||||
assert.equal(files[0]!.filename, 'episode.eng.ass');
|
||||
assert.equal(files[0]!.url, 'https://animetosho.org/storage/attach/001dd61c/1955356.xz');
|
||||
assert.equal(files[0]!.filename, 'episode.en.ass');
|
||||
assert.equal(files[0]!.url, 'https://storage.tsukihime.org/attach/001dd61c/1955356.xz');
|
||||
});
|
||||
|
||||
@@ -3,8 +3,8 @@ import { AnkiIntegration } from '../../anki-integration';
|
||||
import { mergeAiConfig } from '../../ai/config';
|
||||
import {
|
||||
AiConfig,
|
||||
AnimetoshoApiResponse,
|
||||
AnimetoshoConfig,
|
||||
TsukihimeApiResponse,
|
||||
TsukihimeConfig,
|
||||
AnkiConnectConfig,
|
||||
JimakuApiResponse,
|
||||
JimakuEntry,
|
||||
@@ -17,13 +17,13 @@ import {
|
||||
} from '../../types';
|
||||
import { sortJimakuFiles } from '../../jimaku/utils';
|
||||
import {
|
||||
ANIMETOSHO_FEED_BASE_URL,
|
||||
animetoshoFetchJson as animetoshoFetchJsonRequest,
|
||||
TSUKIHIME_API_BASE_URL,
|
||||
tsukihimeFetchJson as tsukihimeFetchJsonRequest,
|
||||
decompressXzFile,
|
||||
extractAnimetoshoSubtitleFiles,
|
||||
isAnimetoshoDownloadUrl,
|
||||
mapAnimetoshoSearchResults,
|
||||
} from '../../animetosho/utils';
|
||||
extractTsukihimeSubtitleFiles,
|
||||
isTsukihimeDownloadUrl,
|
||||
mapTsukihimeSearchResults,
|
||||
} from '../../tsukihime/utils';
|
||||
import type { AnkiJimakuIpcDeps } from './anki-jimaku-ipc';
|
||||
import { createLogger } from '../../logger';
|
||||
|
||||
@@ -48,7 +48,7 @@ export interface AnkiJimakuIpcRuntimeOptions {
|
||||
getResolvedConfig: () => {
|
||||
ankiConnect?: AnkiConnectConfig;
|
||||
ai?: AiConfig;
|
||||
animetosho?: AnimetoshoConfig;
|
||||
tsukihime?: TsukihimeConfig;
|
||||
secondarySub?: { secondarySubLanguages?: string[] };
|
||||
};
|
||||
getRuntimeOptionsManager: () => RuntimeOptionsManagerLike | null;
|
||||
@@ -77,10 +77,10 @@ export interface AnkiJimakuIpcRuntimeOptions {
|
||||
endpoint: string,
|
||||
query?: Record<string, string | number | boolean | null | undefined>,
|
||||
) => Promise<JimakuApiResponse<T>>;
|
||||
animetoshoFetchJson?: <T>(
|
||||
tsukihimeFetchJson?: <T>(
|
||||
endpoint: string,
|
||||
query?: Record<string, string | number | boolean | null | undefined>,
|
||||
) => Promise<AnimetoshoApiResponse<T>>;
|
||||
) => Promise<TsukihimeApiResponse<T>>;
|
||||
getJimakuMaxEntryResults: () => number;
|
||||
getJimakuLanguagePreference: () => JimakuLanguagePreference;
|
||||
resolveJimakuApiKey: () => Promise<string | null>;
|
||||
@@ -101,7 +101,7 @@ export interface AnkiJimakuIpcRuntimeOptions {
|
||||
|
||||
const logger = createLogger('main:anki-jimaku');
|
||||
|
||||
const DEFAULT_ANIMETOSHO_MAX_SEARCH_RESULTS = 10;
|
||||
const DEFAULT_TSUKIHIME_MAX_SEARCH_RESULTS = 10;
|
||||
const SECONDARY_TRACK_LOOKUP_ATTEMPTS = 5;
|
||||
const SECONDARY_TRACK_LOOKUP_RETRY_MS = 100;
|
||||
|
||||
@@ -109,24 +109,24 @@ function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function getAnimetoshoMaxSearchResults(options: AnkiJimakuIpcRuntimeOptions): number {
|
||||
const value = options.getResolvedConfig().animetosho?.maxSearchResults;
|
||||
function getTsukihimeMaxSearchResults(options: AnkiJimakuIpcRuntimeOptions): number {
|
||||
const value = options.getResolvedConfig().tsukihime?.maxSearchResults;
|
||||
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
||||
return Math.floor(value);
|
||||
}
|
||||
return DEFAULT_ANIMETOSHO_MAX_SEARCH_RESULTS;
|
||||
return DEFAULT_TSUKIHIME_MAX_SEARCH_RESULTS;
|
||||
}
|
||||
|
||||
function animetoshoFetch<T>(
|
||||
function tsukihimeFetch<T>(
|
||||
options: AnkiJimakuIpcRuntimeOptions,
|
||||
endpoint: string,
|
||||
query: Record<string, string | number | boolean | null | undefined>,
|
||||
): Promise<AnimetoshoApiResponse<T>> {
|
||||
if (options.animetoshoFetchJson) {
|
||||
return options.animetoshoFetchJson<T>(endpoint, query);
|
||||
): Promise<TsukihimeApiResponse<T>> {
|
||||
if (options.tsukihimeFetchJson) {
|
||||
return options.tsukihimeFetchJson<T>(endpoint, query);
|
||||
}
|
||||
const baseUrl = options.getResolvedConfig().animetosho?.apiBaseUrl || ANIMETOSHO_FEED_BASE_URL;
|
||||
return animetoshoFetchJsonRequest<T>(endpoint, query, { baseUrl });
|
||||
const baseUrl = options.getResolvedConfig().tsukihime?.apiBaseUrl || TSUKIHIME_API_BASE_URL;
|
||||
return tsukihimeFetchJsonRequest<T>(endpoint, query, { baseUrl });
|
||||
}
|
||||
|
||||
export function registerAnkiJimakuIpcRuntime(
|
||||
@@ -242,39 +242,41 @@ export function registerAnkiJimakuIpcRuntime(
|
||||
isRemoteMediaPath: (mediaPath) => options.isRemoteMediaPath(mediaPath),
|
||||
downloadToFile: (url, destPath, headers) => options.downloadToFile(url, destPath, headers),
|
||||
|
||||
searchAnimetoshoEntries: async (query) => {
|
||||
logger.info(`[animetosho] search-entries query: "${query.query}"`);
|
||||
const response = await animetoshoFetch<unknown>(options, '/json', {
|
||||
searchTsukihimeEntries: async (query) => {
|
||||
logger.info(`[tsukihime] search-entries query: "${query.query}"`);
|
||||
const maxResults = getTsukihimeMaxSearchResults(options);
|
||||
const response = await tsukihimeFetch<unknown>(options, '/search/torrents', {
|
||||
q: query.query,
|
||||
qx: 1,
|
||||
// The API caps limit at 100.
|
||||
limit: Math.min(maxResults, 100),
|
||||
});
|
||||
if (!response.ok) return response;
|
||||
const maxResults = getAnimetoshoMaxSearchResults(options);
|
||||
const entries = mapAnimetoshoSearchResults(response.data, maxResults);
|
||||
logger.info(`[animetosho] search-entries returned ${entries.length} results`);
|
||||
const entries = mapTsukihimeSearchResults(response.data, maxResults);
|
||||
logger.info(`[tsukihime] search-entries returned ${entries.length} results`);
|
||||
return { ok: true, data: entries };
|
||||
},
|
||||
listAnimetoshoFiles: async (query) => {
|
||||
logger.info(`[animetosho] list-files entryId=${query.entryId}`);
|
||||
const response = await animetoshoFetch<unknown>(options, '/json', {
|
||||
show: 'torrent',
|
||||
id: query.entryId,
|
||||
});
|
||||
listTsukihimeFiles: async (query) => {
|
||||
logger.info(`[tsukihime] list-files entryId=${query.entryId}`);
|
||||
const response = await tsukihimeFetch<unknown>(
|
||||
options,
|
||||
`/torrents/${encodeURIComponent(query.entryId)}`,
|
||||
{},
|
||||
);
|
||||
if (!response.ok) return response;
|
||||
const files = extractAnimetoshoSubtitleFiles(response.data);
|
||||
logger.info(`[animetosho] list-files returned ${files.length} subtitle attachments`);
|
||||
const files = extractTsukihimeSubtitleFiles(response.data);
|
||||
logger.info(`[tsukihime] list-files returned ${files.length} subtitle attachments`);
|
||||
return { ok: true, data: files };
|
||||
},
|
||||
getAnimetoshoSecondaryLanguages: () =>
|
||||
getTsukihimeSecondaryLanguages: () =>
|
||||
options.getResolvedConfig().secondarySub?.secondarySubLanguages ?? [],
|
||||
downloadAnimetoshoSubtitle: async (url, destPath) => {
|
||||
downloadTsukihimeSubtitle: async (url, destPath) => {
|
||||
const tempXzPath = `${destPath}.xz`;
|
||||
const downloaded = await options.downloadToFile(
|
||||
url,
|
||||
tempXzPath,
|
||||
{ 'User-Agent': 'SubMiner' },
|
||||
// animetosho.org redirects to storage.animetosho.org; keep the hop in-domain.
|
||||
{ isAllowedRedirect: (redirectUrl) => isAnimetoshoDownloadUrl(redirectUrl) },
|
||||
// The /tosho/ mirror 302s to storage.animetosho.org; keep the hop in-allowlist.
|
||||
{ isAllowedRedirect: (redirectUrl) => isTsukihimeDownloadUrl(redirectUrl) },
|
||||
);
|
||||
if (!downloaded.ok) return downloaded;
|
||||
const result = await decompressXzFile(tempXzPath, destPath);
|
||||
@@ -310,14 +312,14 @@ export function registerAnkiJimakuIpcRuntime(
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('[animetosho] failed to select downloaded subtitle as secondary:', error);
|
||||
logger.warn('[tsukihime] failed to select downloaded subtitle as secondary:', error);
|
||||
return;
|
||||
}
|
||||
await delay(SECONDARY_TRACK_LOOKUP_RETRY_MS);
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
`[animetosho] could not find downloaded subtitle in track-list: ${pathToSubtitle}`,
|
||||
`[tsukihime] could not find downloaded subtitle in track-list: ${pathToSubtitle}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
openControllerSelect: false,
|
||||
openControllerDebug: false,
|
||||
openJimaku: false,
|
||||
openAnimetosho: false,
|
||||
openTsukihime: false,
|
||||
openYoutubePicker: false,
|
||||
openPlaylistBrowser: false,
|
||||
replayCurrentSubtitle: false,
|
||||
|
||||
@@ -45,7 +45,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
openControllerSelect: false,
|
||||
openControllerDebug: false,
|
||||
openJimaku: false,
|
||||
openAnimetosho: false,
|
||||
openTsukihime: false,
|
||||
openYoutubePicker: false,
|
||||
openPlaylistBrowser: false,
|
||||
togglePrimarySubtitleBar: false,
|
||||
|
||||
@@ -544,11 +544,11 @@ export function handleCliCommand(
|
||||
);
|
||||
} else if (args.openJimaku) {
|
||||
dispatchCliSessionAction({ actionId: 'openJimaku' }, 'openJimaku', 'Open jimaku failed');
|
||||
} else if (args.openAnimetosho) {
|
||||
} else if (args.openTsukihime) {
|
||||
dispatchCliSessionAction(
|
||||
{ actionId: 'openAnimetosho' },
|
||||
'openAnimetosho',
|
||||
'Open animetosho failed',
|
||||
{ actionId: 'openTsukihime' },
|
||||
'openTsukihime',
|
||||
'Open tsukihime failed',
|
||||
);
|
||||
} else if (args.openYoutubePicker) {
|
||||
dispatchCliSessionAction(
|
||||
|
||||
@@ -13,6 +13,7 @@ function createOptions(overrides: Partial<Parameters<typeof handleMpvCommandFrom
|
||||
RUNTIME_OPTIONS_OPEN: '__runtime-options-open',
|
||||
JIMAKU_OPEN: '__jimaku-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',
|
||||
@@ -28,8 +29,8 @@ function createOptions(overrides: Partial<Parameters<typeof handleMpvCommandFrom
|
||||
openJimaku: () => {
|
||||
calls.push('jimaku');
|
||||
},
|
||||
openAnimetosho: () => {
|
||||
calls.push('animetosho');
|
||||
openTsukihime: () => {
|
||||
calls.push('tsukihime');
|
||||
},
|
||||
openYoutubeTrackPicker: () => {
|
||||
calls.push('youtube-picker');
|
||||
@@ -152,6 +153,15 @@ test('handleMpvCommandFromIpc dispatches special jimaku open command', () => {
|
||||
assert.deepEqual(osd, []);
|
||||
});
|
||||
|
||||
test('handleMpvCommandFromIpc keeps the legacy Animetosho command as a TsukiHime alias', () => {
|
||||
const { options, calls, sentCommands } = createOptions();
|
||||
|
||||
handleMpvCommandFromIpc(['__animetosho-open'], options);
|
||||
|
||||
assert.deepEqual(calls, ['tsukihime']);
|
||||
assert.deepEqual(sentCommands, []);
|
||||
});
|
||||
|
||||
test('handleMpvCommandFromIpc dispatches special playlist browser open command', async () => {
|
||||
const { options, calls, sentCommands, osd } = createOptions();
|
||||
handleMpvCommandFromIpc(['__playlist-browser-open'], options);
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface HandleMpvCommandFromIpcOptions {
|
||||
RUNTIME_OPTIONS_OPEN: string;
|
||||
JIMAKU_OPEN: string;
|
||||
ANIMETOSHO_OPEN: string;
|
||||
TSUKIHIME_OPEN: string;
|
||||
RUNTIME_OPTION_CYCLE_PREFIX: string;
|
||||
REPLAY_SUBTITLE: string;
|
||||
PLAY_NEXT_SUBTITLE: string;
|
||||
@@ -20,7 +21,7 @@ export interface HandleMpvCommandFromIpcOptions {
|
||||
triggerSubsyncFromConfig: () => void;
|
||||
openRuntimeOptionsPalette: () => void;
|
||||
openJimaku: () => void;
|
||||
openAnimetosho: () => void;
|
||||
openTsukihime: () => void;
|
||||
openYoutubeTrackPicker: () => void | Promise<void>;
|
||||
openPlaylistBrowser: () => void | Promise<void>;
|
||||
runtimeOptionsCycle: (id: RuntimeOptionId, direction: 1 | -1) => RuntimeOptionApplyResult;
|
||||
@@ -114,8 +115,11 @@ export function handleMpvCommandFromIpc(
|
||||
return;
|
||||
}
|
||||
|
||||
if (first === options.specialCommands.ANIMETOSHO_OPEN) {
|
||||
options.openAnimetosho();
|
||||
if (
|
||||
first === options.specialCommands.TSUKIHIME_OPEN ||
|
||||
first === options.specialCommands.ANIMETOSHO_OPEN
|
||||
) {
|
||||
options.openTsukihime();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ function makeShortcuts(overrides: Partial<ConfiguredShortcuts> = {}): Configured
|
||||
openCharacterDictionaryManager: null,
|
||||
openRuntimeOptions: null,
|
||||
openJimaku: null,
|
||||
openAnimetosho: null,
|
||||
openTsukihime: null,
|
||||
openSessionHelp: null,
|
||||
openControllerSelect: null,
|
||||
openControllerDebug: null,
|
||||
@@ -55,8 +55,8 @@ function createDeps(overrides: Partial<OverlayShortcutRuntimeDeps> = {}) {
|
||||
openJimaku: () => {
|
||||
calls.push('openJimaku');
|
||||
},
|
||||
openAnimetosho: () => {
|
||||
calls.push('openAnimetosho');
|
||||
openTsukihime: () => {
|
||||
calls.push('openTsukihime');
|
||||
},
|
||||
markAudioCard: async () => {
|
||||
calls.push('markAudioCard');
|
||||
@@ -168,7 +168,7 @@ test('runOverlayShortcutLocalFallback dispatches matching single-step actions',
|
||||
openRuntimeOptions: () => handled.push('openRuntimeOptions'),
|
||||
openCharacterDictionaryManager: () => handled.push('openCharacterDictionaryManager'),
|
||||
openJimaku: () => handled.push('openJimaku'),
|
||||
openAnimetosho: () => handled.push('openAnimetosho'),
|
||||
openTsukihime: () => handled.push('openTsukihime'),
|
||||
markAudioCard: () => handled.push('markAudioCard'),
|
||||
copySubtitleMultiple: (timeoutMs) => handled.push(`copySubtitleMultiple:${timeoutMs}`),
|
||||
copySubtitle: () => handled.push('copySubtitle'),
|
||||
@@ -202,7 +202,7 @@ test('runOverlayShortcutLocalFallback leaves multi-step numeric shortcuts for re
|
||||
openRuntimeOptions: () => handled.push('openRuntimeOptions'),
|
||||
openCharacterDictionaryManager: () => handled.push('openCharacterDictionaryManager'),
|
||||
openJimaku: () => handled.push('openJimaku'),
|
||||
openAnimetosho: () => handled.push('openAnimetosho'),
|
||||
openTsukihime: () => handled.push('openTsukihime'),
|
||||
markAudioCard: () => handled.push('markAudioCard'),
|
||||
copySubtitleMultiple: (timeoutMs) => handled.push(`copySubtitleMultiple:${timeoutMs}`),
|
||||
copySubtitle: () => handled.push('copySubtitle'),
|
||||
@@ -223,7 +223,7 @@ test('runOverlayShortcutLocalFallback leaves multi-step numeric shortcuts for re
|
||||
openRuntimeOptions: () => handled.push('openRuntimeOptions'),
|
||||
openCharacterDictionaryManager: () => handled.push('openCharacterDictionaryManager'),
|
||||
openJimaku: () => handled.push('openJimaku'),
|
||||
openAnimetosho: () => handled.push('openAnimetosho'),
|
||||
openTsukihime: () => handled.push('openTsukihime'),
|
||||
markAudioCard: () => handled.push('markAudioCard'),
|
||||
copySubtitleMultiple: (timeoutMs) => handled.push(`copySubtitleMultiple:${timeoutMs}`),
|
||||
copySubtitle: () => handled.push('copySubtitle'),
|
||||
@@ -261,7 +261,7 @@ test('runOverlayShortcutLocalFallback passes allowWhenRegistered for secondary-s
|
||||
openRuntimeOptions: () => {},
|
||||
openCharacterDictionaryManager: () => {},
|
||||
openJimaku: () => {},
|
||||
openAnimetosho: () => {},
|
||||
openTsukihime: () => {},
|
||||
markAudioCard: () => {},
|
||||
copySubtitleMultiple: () => {},
|
||||
copySubtitle: () => {},
|
||||
@@ -298,7 +298,7 @@ test('runOverlayShortcutLocalFallback allows registered-global jimaku shortcut',
|
||||
openRuntimeOptions: () => {},
|
||||
openCharacterDictionaryManager: () => {},
|
||||
openJimaku: () => {},
|
||||
openAnimetosho: () => {},
|
||||
openTsukihime: () => {},
|
||||
markAudioCard: () => {},
|
||||
copySubtitleMultiple: () => {},
|
||||
copySubtitle: () => {},
|
||||
@@ -331,7 +331,7 @@ test('runOverlayShortcutLocalFallback returns false when no action matches', ()
|
||||
openJimaku: () => {
|
||||
called = true;
|
||||
},
|
||||
openAnimetosho: () => {
|
||||
openTsukihime: () => {
|
||||
called = true;
|
||||
},
|
||||
markAudioCard: () => {
|
||||
@@ -416,7 +416,7 @@ test('registerOverlayShortcutsRuntime reports active shortcuts when configured',
|
||||
openCharacterDictionaryManager: () => {},
|
||||
openRuntimeOptions: () => {},
|
||||
openJimaku: () => {},
|
||||
openAnimetosho: () => {},
|
||||
openTsukihime: () => {},
|
||||
}),
|
||||
cancelPendingMultiCopy: () => {},
|
||||
cancelPendingMineSentenceMultiple: () => {},
|
||||
@@ -444,7 +444,7 @@ test('unregisterOverlayShortcutsRuntime clears pending shortcut work when active
|
||||
openCharacterDictionaryManager: () => {},
|
||||
openRuntimeOptions: () => {},
|
||||
openJimaku: () => {},
|
||||
openAnimetosho: () => {},
|
||||
openTsukihime: () => {},
|
||||
}),
|
||||
cancelPendingMultiCopy: () => {
|
||||
calls.push('cancel-multi-copy');
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface OverlayShortcutFallbackHandlers {
|
||||
openRuntimeOptions: () => void;
|
||||
openCharacterDictionaryManager: () => void;
|
||||
openJimaku: () => void;
|
||||
openAnimetosho: () => void;
|
||||
openTsukihime: () => void;
|
||||
markAudioCard: () => void;
|
||||
copySubtitleMultiple: (timeoutMs: number) => void;
|
||||
copySubtitle: () => void;
|
||||
@@ -25,7 +25,7 @@ export interface OverlayShortcutRuntimeDeps {
|
||||
openRuntimeOptions: () => void;
|
||||
openCharacterDictionaryManager: () => void;
|
||||
openJimaku: () => void;
|
||||
openAnimetosho: () => void;
|
||||
openTsukihime: () => void;
|
||||
markAudioCard: () => Promise<void>;
|
||||
copySubtitleMultiple: (timeoutMs: number) => void;
|
||||
copySubtitle: () => void;
|
||||
@@ -105,8 +105,8 @@ export function createOverlayShortcutRuntimeHandlers(deps: OverlayShortcutRuntim
|
||||
openJimaku: () => {
|
||||
deps.openJimaku();
|
||||
},
|
||||
openAnimetosho: () => {
|
||||
deps.openAnimetosho();
|
||||
openTsukihime: () => {
|
||||
deps.openTsukihime();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -114,7 +114,7 @@ export function createOverlayShortcutRuntimeHandlers(deps: OverlayShortcutRuntim
|
||||
openRuntimeOptions: overlayHandlers.openRuntimeOptions,
|
||||
openCharacterDictionaryManager: overlayHandlers.openCharacterDictionaryManager,
|
||||
openJimaku: overlayHandlers.openJimaku,
|
||||
openAnimetosho: overlayHandlers.openAnimetosho,
|
||||
openTsukihime: overlayHandlers.openTsukihime,
|
||||
markAudioCard: overlayHandlers.markAudioCard,
|
||||
copySubtitleMultiple: overlayHandlers.copySubtitleMultiple,
|
||||
copySubtitle: overlayHandlers.copySubtitle,
|
||||
@@ -160,9 +160,9 @@ export function runOverlayShortcutLocalFallback(
|
||||
allowWhenRegistered: true,
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.openAnimetosho,
|
||||
accelerator: shortcuts.openTsukihime,
|
||||
run: () => {
|
||||
handlers.openAnimetosho();
|
||||
handlers.openTsukihime();
|
||||
},
|
||||
allowWhenRegistered: true,
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@ function createShortcuts(overrides: Partial<ConfiguredShortcuts> = {}): Configur
|
||||
openCharacterDictionaryManager: null,
|
||||
openRuntimeOptions: null,
|
||||
openJimaku: null,
|
||||
openAnimetosho: null,
|
||||
openTsukihime: null,
|
||||
openSessionHelp: null,
|
||||
openControllerSelect: null,
|
||||
openControllerDebug: null,
|
||||
@@ -49,7 +49,7 @@ test('registerOverlayShortcuts reports active overlay shortcuts when configured'
|
||||
openCharacterDictionaryManager: () => {},
|
||||
openRuntimeOptions: () => {},
|
||||
openJimaku: () => {},
|
||||
openAnimetosho: () => {},
|
||||
openTsukihime: () => {},
|
||||
}),
|
||||
true,
|
||||
);
|
||||
@@ -70,7 +70,7 @@ test('registerOverlayShortcuts stays inactive when overlay shortcuts are absent'
|
||||
openCharacterDictionaryManager: () => {},
|
||||
openRuntimeOptions: () => {},
|
||||
openJimaku: () => {},
|
||||
openAnimetosho: () => {},
|
||||
openTsukihime: () => {},
|
||||
}),
|
||||
false,
|
||||
);
|
||||
@@ -93,7 +93,7 @@ test('syncOverlayShortcutsRuntime deactivates cleanly when shortcuts were active
|
||||
openCharacterDictionaryManager: () => {},
|
||||
openRuntimeOptions: () => {},
|
||||
openJimaku: () => {},
|
||||
openAnimetosho: () => {},
|
||||
openTsukihime: () => {},
|
||||
}),
|
||||
cancelPendingMultiCopy: () => {
|
||||
calls.push('cancel-multi-copy');
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface OverlayShortcutHandlers {
|
||||
openCharacterDictionaryManager: () => void;
|
||||
openRuntimeOptions: () => void;
|
||||
openJimaku: () => void;
|
||||
openAnimetosho: () => void;
|
||||
openTsukihime: () => void;
|
||||
}
|
||||
|
||||
export interface OverlayShortcutLifecycleDeps {
|
||||
@@ -36,7 +36,7 @@ const OVERLAY_SHORTCUT_KEYS: Array<keyof Omit<ConfiguredShortcuts, 'multiCopyTim
|
||||
'openCharacterDictionaryManager',
|
||||
'openRuntimeOptions',
|
||||
'openJimaku',
|
||||
'openAnimetosho',
|
||||
'openTsukihime',
|
||||
];
|
||||
|
||||
function hasConfiguredOverlayShortcuts(shortcuts: ConfiguredShortcuts): boolean {
|
||||
|
||||
@@ -40,7 +40,7 @@ function createDeps(overrides: Partial<SessionActionExecutorDeps> = {}) {
|
||||
openControllerSelect: () => calls.push('controller-select'),
|
||||
openControllerDebug: () => calls.push('controller-debug'),
|
||||
openJimaku: () => calls.push('jimaku'),
|
||||
openAnimetosho: () => calls.push('animetosho'),
|
||||
openTsukihime: () => calls.push('tsukihime'),
|
||||
openYoutubeTrackPicker: () => {
|
||||
calls.push('youtube');
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface SessionActionExecutorDeps {
|
||||
openControllerSelect: () => void;
|
||||
openControllerDebug: () => void;
|
||||
openJimaku: () => void;
|
||||
openAnimetosho: () => void;
|
||||
openTsukihime: () => void;
|
||||
openYoutubeTrackPicker: () => void | Promise<void>;
|
||||
openPlaylistBrowser: () => boolean | void | Promise<boolean | void>;
|
||||
replayCurrentSubtitle: () => void;
|
||||
@@ -116,8 +116,8 @@ export async function dispatchSessionAction(
|
||||
case 'openJimaku':
|
||||
deps.openJimaku();
|
||||
return;
|
||||
case 'openAnimetosho':
|
||||
deps.openAnimetosho();
|
||||
case 'openTsukihime':
|
||||
deps.openTsukihime();
|
||||
return;
|
||||
case 'openYoutubePicker':
|
||||
await deps.openYoutubeTrackPicker();
|
||||
|
||||
@@ -22,7 +22,7 @@ function createShortcuts(overrides: Partial<ConfiguredShortcuts> = {}): Configur
|
||||
openCharacterDictionaryManager: null,
|
||||
openRuntimeOptions: null,
|
||||
openJimaku: null,
|
||||
openAnimetosho: null,
|
||||
openTsukihime: null,
|
||||
openSessionHelp: null,
|
||||
openControllerSelect: null,
|
||||
openControllerDebug: null,
|
||||
@@ -320,6 +320,21 @@ test('compileSessionBindings wires every default keybinding to an overlay or mpv
|
||||
}
|
||||
});
|
||||
|
||||
test('compileSessionBindings maps the legacy Animetosho command to the TsukiHime action', () => {
|
||||
const result = compileSessionBindings({
|
||||
shortcuts: createShortcuts(),
|
||||
keybindings: [createKeybinding('Ctrl+Alt+T', ['__animetosho-open'])],
|
||||
platform: 'linux',
|
||||
});
|
||||
|
||||
assert.deepEqual(result.warnings, []);
|
||||
assert.equal(result.bindings[0]?.actionType, 'session-action');
|
||||
assert.equal(
|
||||
result.bindings[0]?.actionType === 'session-action' ? result.bindings[0].actionId : null,
|
||||
'openTsukihime',
|
||||
);
|
||||
});
|
||||
|
||||
test('compileSessionBindings leaves retired subtitle-delay shift tokens as mpv commands', () => {
|
||||
const result = compileSessionBindings({
|
||||
shortcuts: createShortcuts(),
|
||||
|
||||
@@ -55,7 +55,7 @@ const SESSION_SHORTCUT_ACTIONS: Array<{
|
||||
{ key: 'openCharacterDictionaryManager', actionId: 'openCharacterDictionaryManager' },
|
||||
{ key: 'openRuntimeOptions', actionId: 'openRuntimeOptions' },
|
||||
{ key: 'openJimaku', actionId: 'openJimaku' },
|
||||
{ key: 'openAnimetosho', actionId: 'openAnimetosho' },
|
||||
{ key: 'openTsukihime', actionId: 'openTsukihime' },
|
||||
{ key: 'openSessionHelp', actionId: 'openSessionHelp' },
|
||||
{ key: 'openControllerSelect', actionId: 'openControllerSelect' },
|
||||
{ key: 'openControllerDebug', actionId: 'openControllerDebug' },
|
||||
@@ -305,9 +305,9 @@ function resolveCommandBinding(
|
||||
if (command.length !== 1) return null;
|
||||
return { actionType: 'session-action', actionId: 'openJimaku' };
|
||||
}
|
||||
if (first === SPECIAL_COMMANDS.ANIMETOSHO_OPEN) {
|
||||
if (first === SPECIAL_COMMANDS.TSUKIHIME_OPEN || first === SPECIAL_COMMANDS.ANIMETOSHO_OPEN) {
|
||||
if (command.length !== 1) return null;
|
||||
return { actionType: 'session-action', actionId: 'openAnimetosho' };
|
||||
return { actionType: 'session-action', actionId: 'openTsukihime' };
|
||||
}
|
||||
if (first === SPECIAL_COMMANDS.YOUTUBE_PICKER_OPEN) {
|
||||
if (command.length !== 1) return null;
|
||||
|
||||
@@ -40,7 +40,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
openControllerSelect: false,
|
||||
openControllerDebug: false,
|
||||
openJimaku: false,
|
||||
openAnimetosho: false,
|
||||
openTsukihime: false,
|
||||
openYoutubePicker: false,
|
||||
openPlaylistBrowser: false,
|
||||
replayCurrentSubtitle: false,
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface ConfiguredShortcuts {
|
||||
openCharacterDictionaryManager: string | null | undefined;
|
||||
openRuntimeOptions: string | null | undefined;
|
||||
openJimaku: string | null | undefined;
|
||||
openAnimetosho: string | null | undefined;
|
||||
openTsukihime: string | null | undefined;
|
||||
openSessionHelp: string | null | undefined;
|
||||
openControllerSelect: string | null | undefined;
|
||||
openControllerDebug: string | null | undefined;
|
||||
@@ -66,7 +66,7 @@ export function resolveConfiguredShortcuts(
|
||||
),
|
||||
openRuntimeOptions: normalizeShortcut(shortcutValue('openRuntimeOptions')),
|
||||
openJimaku: normalizeShortcut(shortcutValue('openJimaku')),
|
||||
openAnimetosho: normalizeShortcut(shortcutValue('openAnimetosho')),
|
||||
openTsukihime: normalizeShortcut(shortcutValue('openTsukihime')),
|
||||
openSessionHelp: normalizeShortcut(shortcutValue('openSessionHelp')),
|
||||
openControllerSelect: normalizeShortcut(shortcutValue('openControllerSelect')),
|
||||
openControllerDebug: normalizeShortcut(shortcutValue('openControllerDebug')),
|
||||
|
||||
Reference in New Issue
Block a user