fix(tsukihime): preserve Animetosho aliases

This commit is contained in:
2026-07-13 03:33:22 -07:00
parent ef9bf26efd
commit f4a5424fb0
18 changed files with 165 additions and 14 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
type: added
area: overlay
- Added a TsukiHime integration for downloading English (and Japanese) subtitles, mirroring the Jimaku flow: `Ctrl+Shift+T` (configurable via `shortcuts.openTsukihime`) opens an in-overlay modal with two language tabs (the first follows `secondarySub.secondarySubLanguages`, defaulting to English; the second is Japanese) that parses the current video filename, searches TsukiHime releases, lists extracted text subtitle tracks filtered by the active tab, then downloads the chosen track, decompresses it (requires the `xz` binary), saves it next to the video with a language suffix (`<video>.en.<ext>`, `.ja` for Japanese tracks, etc.), and loads it into mpv immediately - Japanese tracks as the primary subtitle, other languages as the secondary subtitle. No API key is required; also reachable via `subminer --open-tsukihime`, the `__tsukihime-open` keybinding command, and configurable under a new `tsukihime` config section. TsukiHime is the successor to Animetosho, which stops processing new releases in May 2026; it imported the Animetosho index and mirrors its attachment storage, so older releases stay reachable.
- Added a TsukiHime integration for downloading English (and Japanese) subtitles, mirroring the Jimaku flow: `Ctrl+Shift+T` (configurable via `shortcuts.openTsukihime`) opens an in-overlay modal with two language tabs (the first follows `secondarySub.secondarySubLanguages`, defaulting to English; the second is Japanese) that parses the current video filename, searches TsukiHime releases, lists extracted text subtitle tracks filtered by the active tab, then downloads the chosen track, decompresses it (requires the `xz` binary), saves it next to the video with a language suffix (`<video>.en.<ext>`, `.ja` for Japanese tracks, etc.), and loads it into mpv immediately - Japanese tracks as the primary subtitle, other languages as the secondary subtitle. No API key is required; also reachable via `subminer --open-tsukihime`, the `__tsukihime-open` keybinding command, and configurable under a new `tsukihime` config section. TsukiHime is the successor to Animetosho, which stops processing new releases in May 2026; it imported the Animetosho index and mirrors its attachment storage, so older releases stay reachable. Existing `animetosho`, `shortcuts.openAnimetosho`, `--open-animetosho`, and `__animetosho-open` inputs remain accepted as deprecated aliases.
+2
View File
@@ -91,6 +91,8 @@ Mouse-hover playback behavior is configured separately from shortcuts: `subtitle
| `` ` `` | Toggle stats overlay | `stats.toggleKey` |
| `W` | Mark current video watched and advance to next in queue | `stats.markWatchedKey` |
`shortcuts.openAnimetosho` remains accepted as a deprecated alias for `shortcuts.openTsukihime`. The current name takes precedence when both are configured.
The stats toggle is handled inside the focused visible overlay window. It is configurable through the top-level `stats.toggleKey` setting and defaults to `Backquote`.
The subtitle sidebar toggle is overlay-local and only opens when SubMiner has a parsed cue list for the active subtitle source.
+4
View File
@@ -64,11 +64,15 @@ The keyboard shortcut is configured separately under `shortcuts`:
}
```
Existing Animetosho configuration remains compatible. SubMiner treats the old `animetosho` section and `shortcuts.openAnimetosho` setting as deprecated aliases. When old and current names are both present, `tsukihime` and `shortcuts.openTsukihime` take precedence.
## Other Ways to Open It
- CLI: `subminer --open-tsukihime`
- Keybinding command: bind any key to `["__tsukihime-open"]` in the `keybindings` array
The previous `--open-animetosho` flag and `__animetosho-open` keybinding command remain accepted as deprecated aliases.
## Troubleshooting
- **"xz binary not found"** - install `xz`/`xz-utils` with your package manager.
+3
View File
@@ -146,6 +146,7 @@ SubMiner.AppImage --show-visible-overlay # Force show visible overl
SubMiner.AppImage --hide-visible-overlay # Force hide visible overlay
SubMiner.AppImage --toggle-primary-subtitle-bar # Toggle primary subtitle bar visibility
SubMiner.AppImage --toggle-subtitle-sidebar # Toggle the subtitle sidebar
SubMiner.AppImage --open-tsukihime # Open TsukiHime subtitle search
SubMiner.AppImage --start --dev # Enable app/dev mode only
SubMiner.AppImage --start --debug # Alias for --dev
SubMiner.AppImage --start --log-level debug # Force verbose logging without app/dev mode
@@ -165,6 +166,8 @@ SubMiner.AppImage --dictionary-select --dictionary-anilist-id 21355 # Pin corre
SubMiner.AppImage --help # Show all options
```
The previous `--open-animetosho` flag remains accepted as a deprecated alias for `--open-tsukihime`.
The tray menu includes `Export Logs`, which creates the same sanitized local-date log ZIP as `subminer logs -e` and shows the archive path when complete. Export sanitization masks common PII and secrets, including home-directory usernames, IP addresses, emails, auth/cookie headers, yt-dlp cookie arguments, URL credentials, token/key/password fields, and signed YouTube media URL query strings. The exported copy is sanitized; source log files remain unredacted on disk.
Once Jellyfin is configured, the tray menu includes `Jellyfin Discovery` for starting or stopping cast discovery in the current app session without changing config.
+1 -1
View File
@@ -254,7 +254,7 @@ function M.create(ctx)
return { "--open-runtime-options" }
elseif action_id == "openJimaku" then
return { "--open-jimaku" }
elseif action_id == "openTsukihime" then
elseif action_id == "openTsukihime" or action_id == "openAnimetosho" then
return { "--open-tsukihime" }
elseif action_id == "openYoutubePicker" then
return { "--open-youtube-picker" }
+9
View File
@@ -237,6 +237,14 @@ local ctx = {
actionType = "session-action",
actionId = "openPlaylistBrowser",
},
{
key = {
code = "KeyT",
modifiers = { "ctrl", "alt" },
},
actionType = "session-action",
actionId = "openAnimetosho",
},
{
key = {
code = "KeyH",
@@ -387,6 +395,7 @@ end
local expected_cli_bindings = {
{ keys = "Ctrl+Alt+c", flag = "--open-youtube-picker" },
{ keys = "Ctrl+Alt+p", flag = "--open-playlist-browser" },
{ keys = "Ctrl+Alt+t", flag = "--open-tsukihime" },
{ keys = "Ctrl+H", flag = "--replay-current-subtitle" },
{ keys = "Ctrl+L", flag = "--play-next-subtitle" },
{ keys = "w", flag = "--mark-watched" },
+8
View File
@@ -148,6 +148,14 @@ test('parseArgs captures session action forwarding flags', () => {
assert.equal(shouldStartApp(args), true);
});
test('parseArgs keeps the legacy Animetosho open flag as a TsukiHime alias', () => {
const args = parseArgs(['--open-animetosho']);
assert.equal(args.openTsukihime, true);
assert.equal(hasExplicitCommand(args), true);
assert.equal(shouldStartApp(args), true);
});
test('parseArgs ignores retired subtitle delay shift flags', () => {
const args = parseArgs(['--shift-sub-delay-prev-line', '--shift-sub-delay-next-line']);
+3 -2
View File
@@ -294,8 +294,9 @@ export function parseArgs(argv: string[]): CliArgs {
else if (arg === '--open-controller-select') args.openControllerSelect = true;
else if (arg === '--open-controller-debug') args.openControllerDebug = true;
else if (arg === '--open-jimaku') args.openJimaku = true;
else if (arg === '--open-tsukihime') args.openTsukihime = true;
else if (arg === '--open-youtube-picker') args.openYoutubePicker = true;
else if (arg === '--open-tsukihime' || arg === '--open-animetosho') {
args.openTsukihime = true;
} else if (arg === '--open-youtube-picker') args.openYoutubePicker = true;
else if (arg === '--open-playlist-browser') args.openPlaylistBrowser = true;
else if (arg === '--replay-current-subtitle') args.replayCurrentSubtitle = true;
else if (arg === '--play-next-subtitle') args.playNextSubtitle = true;
+2
View File
@@ -53,6 +53,8 @@ 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',
+14
View File
@@ -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);
+15 -6
View File
@@ -80,17 +80,26 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
}
}
if (isObject(src.tsukihime)) {
const apiBaseUrl = asString(src.tsukihime.apiBaseUrl);
const currentTsukihimeSource = isObject(src.tsukihime) ? src.tsukihime : null;
if (src.tsukihime !== undefined && !currentTsukihimeSource) {
warn('tsukihime', src.tsukihime, resolved.tsukihime, 'Expected object.');
}
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(src.tsukihime.maxSearchResults);
const maxSearchResults = asNumber(tsukihimeSource.maxSearchResults);
if (maxSearchResults !== undefined && Math.floor(maxSearchResults) > 0) {
resolved.tsukihime.maxSearchResults = Math.floor(maxSearchResults);
} else if (src.tsukihime.maxSearchResults !== undefined) {
} else if (tsukihimeSource.maxSearchResults !== undefined) {
warn(
'tsukihime.maxSearchResults',
src.tsukihime.maxSearchResults,
`${tsukihimeSourcePath}.maxSearchResults`,
tsukihimeSource.maxSearchResults,
resolved.tsukihime.maxSearchResults,
'Expected positive number.',
);
+1 -1
View File
@@ -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.',
},
]);
});
+10
View File
@@ -12,6 +12,7 @@ function createOptions(overrides: Partial<Parameters<typeof handleMpvCommandFrom
SUBSYNC_TRIGGER: '__subsync-trigger',
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',
@@ -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);
+5 -1
View File
@@ -10,6 +10,7 @@ export interface HandleMpvCommandFromIpcOptions {
SUBSYNC_TRIGGER: string;
RUNTIME_OPTIONS_OPEN: string;
JIMAKU_OPEN: string;
ANIMETOSHO_OPEN: string;
TSUKIHIME_OPEN: string;
RUNTIME_OPTION_CYCLE_PREFIX: string;
REPLAY_SUBTITLE: string;
@@ -114,7 +115,10 @@ export function handleMpvCommandFromIpc(
return;
}
if (first === options.specialCommands.TSUKIHIME_OPEN) {
if (
first === options.specialCommands.TSUKIHIME_OPEN ||
first === options.specialCommands.ANIMETOSHO_OPEN
) {
options.openTsukihime();
return;
}
@@ -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(),
+1 -1
View File
@@ -305,7 +305,7 @@ function resolveCommandBinding(
if (command.length !== 1) return null;
return { actionType: 'session-action', actionId: 'openJimaku' };
}
if (first === SPECIAL_COMMANDS.TSUKIHIME_OPEN) {
if (first === SPECIAL_COMMANDS.TSUKIHIME_OPEN || first === SPECIAL_COMMANDS.ANIMETOSHO_OPEN) {
if (command.length !== 1) return null;
return { actionType: 'session-action', actionId: 'openTsukihime' };
}
+8 -1
View File
@@ -132,6 +132,11 @@ export interface ShortcutsConfig {
appendClipboardVideoToQueue?: string | null;
}
export interface RawShortcutsConfig extends ShortcutsConfig {
/** @deprecated Use openTsukihime. */
openAnimetosho?: string | null;
}
export interface Config {
subtitlePosition?: SubtitlePosition;
keybindings?: Keybinding[];
@@ -141,7 +146,7 @@ export interface Config {
mpv?: MpvConfig;
controller?: ControllerConfig;
ankiConnect?: AnkiConnectConfig;
shortcuts?: ShortcutsConfig;
shortcuts?: RawShortcutsConfig;
secondarySub?: SecondarySubConfig;
subsync?: SubsyncConfig;
startupWarmups?: StartupWarmupsConfig;
@@ -149,6 +154,8 @@ export interface Config {
subtitleSidebar?: SubtitleSidebarConfig;
auto_start_overlay?: boolean;
jimaku?: JimakuConfig;
/** @deprecated Use tsukihime. */
animetosho?: TsukihimeConfig;
tsukihime?: TsukihimeConfig;
anilist?: AnilistConfig;
yomitan?: YomitanConfig;