vtt to srt

This commit is contained in:
Kyush 2026-06-06 21:29:37 +09:00
commit 3089ab3b40
3 changed files with 91 additions and 9 deletions

View file

@ -11,6 +11,7 @@ import { showStatus, hideProgress, resetProgress } from './ui/progress';
import { showPlaylistSelector } from './ui/selector';
import { DownloadProgress } from './downloader/segment-fetch';
import { guessFilename } from './utils/filename';
import { vttToSrt } from './utils/subtitle';
const detectedStreams = new Map<string, { url: string; type: 'm3u8' | 'vtt'; headers?: Record<string, string>; parsed?: ParsedPlaylist }>();
let panel: HTMLDivElement | null = null;
@ -95,10 +96,10 @@ function addStreamToUI(url: string, parsed?: ParsedPlaylist): void {
});
}
function downloadVtt(blob: Blob, url: string): void {
function downloadSubtitle(blob: Blob, url: string): void {
const baseName = guessFilename(url, 'text/vtt');
const filename = baseName.replace(/\.(mp4|ts|m4a)$/, '.vtt');
log.info(`downloadVtt: ${filename} (${blob.size} bytes)`);
const filename = baseName.replace(/\.(mp4|ts|m4a)$/, '.srt');
log.info(`downloadSubtitle: ${filename} (${blob.size} bytes)`);
const urlObj = URL.createObjectURL(blob);
const target = (typeof unsafeWindow !== 'undefined' ? unsafeWindow : window) as any;
const doc = target.document || document;
@ -219,18 +220,19 @@ async function downloadVttStream(url: string, entry: { url: string; type: 'vtt';
},
onload: (response: any) => {
if (response.status >= 200 && response.status < 300) {
const text = response.responseText || response.response || '';
const blob = new Blob([text], { type: 'text/vtt' });
downloadVtt(blob, url);
const vttText = response.responseText || response.response || '';
const srtText = vttToSrt(vttText);
const blob = new Blob([srtText], { type: 'text/plain' });
downloadSubtitle(blob, url);
resolve();
} else {
log.error(`downloadVttStream: HTTP ${response.status} for ${url}`);
reject(new Error(`VTT download failed: HTTP ${response.status}`));
reject(new Error(`Subtitle download failed: HTTP ${response.status}`));
}
},
onerror: (error: any) => {
log.error(`downloadVttStream: error for ${url}: ${error.error}`);
reject(new Error(`VTT download error: ${error.error}`));
reject(new Error(`Subtitle download error: ${error.error}`));
},
});
});

View file

@ -4,7 +4,7 @@ export function guessFilename(url: string, mime?: string): string {
if (titleMatch) {
if (ext === '.vtt' || mime?.includes('vtt')) {
return `${titleMatch.name} ${titleMatch.volume}.vtt`;
return `${titleMatch.name} ${titleMatch.volume}.srt`;
}
return `${titleMatch.name} ${titleMatch.volume}.mp4`;
}

80
src/utils/subtitle.ts Normal file
View file

@ -0,0 +1,80 @@
export function vttToSrt(vttText: string): string {
const lines = vttText.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
let idx = 0;
while (idx < lines.length && !lines[idx].trim().startsWith('WEBVTT')) {
idx++;
}
if (idx >= lines.length) {
return vttText;
}
idx++;
while (idx < lines.length && lines[idx].trim() !== '') {
idx++;
}
if (idx < lines.length && lines[idx].trim() === '') {
idx++;
}
const remaining = lines.slice(idx);
const blocks = splitBlocks(remaining);
const srtBlocks: string[] = [];
for (let i = 0; i < blocks.length; i++) {
const block = blocks[i];
if (block.length === 0) continue;
const headerLine = block[0].trim();
if (!headerLine.includes('-->')) continue;
const cleaned = cleanCueText(block.slice(1).join('\n'));
const timestampLine = headerLine.replace(/\./g, ',');
srtBlocks.push(`${i + 1}`);
srtBlocks.push(timestampLine);
srtBlocks.push(cleaned);
srtBlocks.push('');
}
return srtBlocks.join('\n').replace(/\n+$/, '\n');
}
function splitBlocks(lines: string[]): string[][] {
const blocks: string[][] = [];
let current: string[] = [];
for (const line of lines) {
if (line.trim() === '') {
if (current.length > 0) {
blocks.push(current);
current = [];
}
} else {
current.push(line);
}
}
if (current.length > 0) {
blocks.push(current);
}
return blocks;
}
function cleanCueText(text: string): string {
return text
.replace(/<\/?(?:c|b|i|u|ruby|rt)(?:\s[^>]*)?>/g, '')
.replace(/<v[^>]*>/g, '')
.replace(/<\s*(?:position|align|line|size|start)\s*=\s*[^>]*>/g, '')
.replace(/<\s*\w+:\w+[^>]*>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.trim();
}