fix(anime): thread episode metadata through stream playback and fix seas

- Split anime browser stream titles into series/season/episode fields (episode-metadata.ts) instead of one joined string, so stats grouping, the mpv title, and the Jimaku/TsukiHime modals all get real values instead of a filename-derived "m3u8"
- Fix AniList season resolver to walk through split-cour sequels and specials without counting them as a season, and to surface rate-limit/network failures instead of falling through to a wrong air-order guess
- Add a Season field to the TsukiHime modal and include later seasons in its search query
- Rewrite WebVTT subtitle tracks as SRT before handing them to alass, since alass has no VTT support and streams serve VTT
- Update CHANGELOG and troubleshooting docs
This commit is contained in:
2026-08-01 23:34:23 -07:00
parent 1eb7f73b3b
commit 36cd19794e
25 changed files with 1721 additions and 69 deletions
+30 -5
View File
@@ -21,6 +21,7 @@ import {
extractSubtitleTrackToFile,
FileExtractionResult,
} from './subsync-extract';
import { convertSubtitleForAlass } from './subsync-alass-input';
import { resolveMpvHttpHeaders, ResolvedMpvHttpHeaders } from './mpv-http-headers';
import { isRemoteMediaPath } from '../../jimaku/utils';
import { createLogger } from '../../logger';
@@ -299,23 +300,45 @@ async function subsyncToReference(
track: targetTrack,
httpHeaders,
});
const replaceTarget = resolved.replace !== false && !targetExtraction.temporary;
const outputPath = buildRetimedPath(targetExtraction.path, replaceTarget);
// alass reads neither the target nor the reference as WebVTT, so both are
// rewritten as SRT first; ffsubsync parses VTT itself. A video reference is
// left alone: alass reads its audio, and there are no cues to convert.
const referenceIsVideo = referenceFilePath === context.videoPath;
let convertedTarget: FileExtractionResult | null = null;
let convertedReference: FileExtractionResult | null = null;
if (engine === 'alass') {
try {
convertedTarget = convertSubtitleForAlass(targetExtraction.path);
convertedReference = referenceIsVideo ? null : convertSubtitleForAlass(referenceFilePath);
} catch (error) {
if (convertedTarget) cleanupTemporaryFile(convertedTarget);
cleanupTemporaryFile(targetExtraction);
const message = `alass synchronization failed: ${(error as Error).message}`;
logger.error(message);
return { ok: false, message };
}
}
const target = convertedTarget ?? targetExtraction;
const referencePath = convertedReference?.path ?? referenceFilePath;
const replaceTarget = resolved.replace !== false && !target.temporary;
const outputPath = buildRetimedPath(target.path, replaceTarget);
logger.info(
`Running ${engine}: target=${targetExtraction.path} reference=${referenceFilePath} output=${outputPath}`,
`Running ${engine}: target=${target.path} reference=${referencePath} output=${outputPath}`,
);
try {
let result: CommandResult;
if (engine === 'alass') {
const alassPath = resolveSubsyncExecutable(resolved.alassPath, 'alass');
result = await runAlassSync(alassPath, referenceFilePath, targetExtraction.path, outputPath);
result = await runAlassSync(alassPath, referencePath, target.path, outputPath);
} else {
const ffsubsyncPath = resolveSubsyncExecutable(resolved.ffsubsyncPath, 'ffsubsync');
result = await runFfsubsyncSync(
ffsubsyncPath,
context.videoPath,
targetExtraction.path,
target.path,
outputPath,
context.audioStreamIndex,
);
@@ -337,6 +360,8 @@ async function subsyncToReference(
message: `Subtitle synchronized with ${engine}`,
};
} finally {
if (convertedReference) cleanupTemporaryFile(convertedReference);
if (convertedTarget) cleanupTemporaryFile(convertedTarget, outputPath);
cleanupTemporaryFile(targetExtraction, outputPath);
}
}