refactor(tsukihime): swap Animetosho backend for TsukiHime API (#165)

This commit is contained in:
2026-07-13 22:03:35 -07:00
committed by GitHub
parent 49b926e08c
commit 2e2ee3f028
90 changed files with 2253 additions and 1695 deletions
+44 -42
View File
@@ -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}`,
);
},
});