vtt to srt
This commit is contained in:
parent
22b8ee2fa0
commit
3089ab3b40
3 changed files with 91 additions and 9 deletions
18
src/index.ts
18
src/index.ts
|
|
@ -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}`));
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
80
src/utils/subtitle.ts
Normal 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(/ /g, ' ')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/g, '&')
|
||||
.trim();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue