fix(subsync): reopen manual modal on sync errors

This commit is contained in:
2026-03-01 14:03:55 -08:00
parent 49434bf0cd
commit 87fe81ad3e
10 changed files with 594 additions and 7 deletions

View File

@@ -23,6 +23,7 @@ import {
parseKikuFieldGroupingChoice,
parseKikuMergePreviewRequest,
} from '../../shared/ipc/validators';
import { buildJimakuSubtitleFilenameFromMediaPath } from './jimaku-download-path';
const logger = createLogger('main:anki-jimaku-ipc');
@@ -148,10 +149,11 @@ export function registerAnkiJimakuIpcHandlers(
if (!safeName) {
return { ok: false, error: { error: 'Invalid subtitle filename.' } };
}
const subtitleFilename = buildJimakuSubtitleFilenameFromMediaPath(currentMediaPath, safeName);
const ext = path.extname(safeName);
const baseName = ext ? safeName.slice(0, -ext.length) : safeName;
let targetPath = path.join(mediaDir, safeName);
const ext = path.extname(subtitleFilename);
const baseName = ext ? subtitleFilename.slice(0, -ext.length) : subtitleFilename;
let targetPath = path.join(mediaDir, subtitleFilename);
if (fs.existsSync(targetPath)) {
targetPath = path.join(mediaDir, `${baseName} (jimaku-${parsedQuery.entryId})${ext}`);
let counter = 2;

View File

@@ -0,0 +1,28 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { buildJimakuSubtitleFilenameFromMediaPath } from './jimaku-download-path.js';
test('buildJimakuSubtitleFilenameFromMediaPath uses media basename + ja + subtitle extension', () => {
assert.equal(
buildJimakuSubtitleFilenameFromMediaPath('/videos/anime.mkv', 'Subs.Release.1080p.srt'),
'anime.ja.srt',
);
});
test('buildJimakuSubtitleFilenameFromMediaPath falls back to .srt when subtitle name has no extension', () => {
assert.equal(
buildJimakuSubtitleFilenameFromMediaPath('/videos/anime.mkv', 'Subs Release'),
'anime.ja.srt',
);
});
test('buildJimakuSubtitleFilenameFromMediaPath supports remote media URLs', () => {
assert.equal(
buildJimakuSubtitleFilenameFromMediaPath(
'https://cdn.example.org/library/Anime%20Episode%2001.mkv?token=abc',
'anything.ass',
),
'Anime Episode 01.ja.ass',
);
});

View File

@@ -0,0 +1,51 @@
import * as path from 'node:path';
const DEFAULT_JIMAKU_LANGUAGE_SUFFIX = 'ja';
const DEFAULT_SUBTITLE_EXTENSION = '.srt';
function stripFileExtension(name: string): string {
const ext = path.extname(name);
return ext ? name.slice(0, -ext.length) : name;
}
function sanitizeFilenameSegment(value: string, fallback: string): string {
const sanitized = value
.replace(/[\\/:*?"<>|]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
return sanitized || fallback;
}
function resolveMediaFilename(mediaPath: string): string {
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(mediaPath)) {
return path.basename(path.resolve(mediaPath));
}
try {
const parsedUrl = new URL(mediaPath);
const decodedPath = decodeURIComponent(parsedUrl.pathname);
const fromPath = path.basename(decodedPath);
if (fromPath) {
return fromPath;
}
return parsedUrl.hostname.replace(/^www\./, '') || 'subtitle';
} catch {
return path.basename(mediaPath);
}
}
export function buildJimakuSubtitleFilenameFromMediaPath(
mediaPath: string,
downloadedSubtitleName: string,
languageSuffix = DEFAULT_JIMAKU_LANGUAGE_SUFFIX,
): string {
const mediaFilename = resolveMediaFilename(mediaPath);
const mediaBasename = sanitizeFilenameSegment(stripFileExtension(mediaFilename), 'subtitle');
const subtitleName = path.basename(downloadedSubtitleName);
const subtitleExt = path.extname(subtitleName) || DEFAULT_SUBTITLE_EXTENSION;
const normalizedLanguageSuffix = sanitizeFilenameSegment(languageSuffix, 'ja').replace(
/\s+/g,
'-',
);
return `${mediaBasename}.${normalizedLanguageSuffix}${subtitleExt}`;
}