kopia lustrzana https://github.com/mifi/lossless-cut
rodzic
d311656990
commit
60712ce01b
|
@ -100,6 +100,12 @@ module.exports = ({ app, mainWindow, newVersion, isStoreBuild }) => {
|
|||
mainWindow.webContents.send('importEdlFile', 'pbf');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: esc(t('Subtitles (SRT)')),
|
||||
click() {
|
||||
mainWindow.webContents.send('importEdlFile', 'srt');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: esc(t('DV Analyzer Summary.txt')),
|
||||
click() {
|
||||
|
@ -135,6 +141,12 @@ module.exports = ({ app, mainWindow, newVersion, isStoreBuild }) => {
|
|||
mainWindow.webContents.send('exportEdlFile', 'tsv-human');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: esc(t('Subtitles (SRT)')),
|
||||
click() {
|
||||
mainWindow.webContents.send('exportEdlFile', 'srt');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: esc(t('Start times as YouTube Chapters')),
|
||||
click() {
|
||||
|
|
|
@ -1825,8 +1825,10 @@ const App = memo(() => {
|
|||
const inputOptions = {
|
||||
open: isFileOpened ? i18n.t('Open the file instead of the current one') : i18n.t('Open the file'),
|
||||
};
|
||||
|
||||
if (isFileOpened) {
|
||||
if (isLlcProject) inputOptions.project = i18n.t('Load segments from the new file, but keep the current media');
|
||||
else if (filePathLowerCase.endsWith('.srt')) inputOptions.subtitles = i18n.t('Convert subtitiles into segments');
|
||||
else inputOptions.tracks = i18n.t('Include all tracks from the new file');
|
||||
}
|
||||
|
||||
|
@ -1844,6 +1846,10 @@ const App = memo(() => {
|
|||
await loadEdlFile({ path: firstFilePath, type: 'llc' });
|
||||
return;
|
||||
}
|
||||
if (openFileResponse === 'subtitles') {
|
||||
await loadEdlFile({ path: firstFilePath, type: 'srt' });
|
||||
return;
|
||||
}
|
||||
if (openFileResponse === 'tracks') {
|
||||
await addStreamSourceFile(firstFilePath);
|
||||
setStreamsSelectorShown(true);
|
||||
|
@ -1861,6 +1867,7 @@ const App = memo(() => {
|
|||
if (batchPaths.size > 1) setConcatDialogVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Dialog canceled:
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,17 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`format srt 1`] = `
|
||||
"1
|
||||
00:00:02,000 --> 00:00:06,000
|
||||
First subtitle
|
||||
|
||||
2
|
||||
00:00:28,967 --> 01:30:30,950
|
||||
Subtitle 2 line 1
|
||||
Subtitle 2 line 2
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`parses DV Analyzer Summary.txt 1`] = `
|
||||
[
|
||||
{
|
||||
|
@ -1013,6 +1025,28 @@ exports[`parses pbf 4`] = `
|
|||
]
|
||||
`;
|
||||
|
||||
exports[`parses srt 1`] = `
|
||||
[
|
||||
{
|
||||
"end": 6,
|
||||
"name": "First subtitle",
|
||||
"start": 2,
|
||||
"tags": {
|
||||
"index": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"end": 5430.95,
|
||||
"name": "Subtitle 2 line 1
|
||||
Subtitle 2 line 2",
|
||||
"start": 28.967,
|
||||
"tags": {
|
||||
"index": 2,
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`parses xmeml - with multiple tracks 1`] = `
|
||||
[
|
||||
{
|
||||
|
|
|
@ -509,7 +509,7 @@ export async function labelSegmentDialog({ currentName, maxLength }) {
|
|||
showCancelButton: true,
|
||||
title: i18n.t('Label current segment'),
|
||||
inputValue: currentName,
|
||||
input: 'text',
|
||||
input: currentName.includes('\n') ? 'textarea' : 'text',
|
||||
inputValidator: (v) => (v.length > maxLength ? `${i18n.t('Max length')} ${maxLength}` : undefined),
|
||||
});
|
||||
return value;
|
||||
|
|
|
@ -295,3 +295,54 @@ export function parseDvAnalyzerSummaryTxt(txt) {
|
|||
|
||||
return edl;
|
||||
}
|
||||
|
||||
// http://www.textfiles.com/uploads/kds-srt.txt
|
||||
export function parseSrt(text) {
|
||||
const ret = [];
|
||||
|
||||
// working state
|
||||
let subtitleIndexAt;
|
||||
let start;
|
||||
let end;
|
||||
let lines = [];
|
||||
|
||||
const flush = () => {
|
||||
if (start != null && end != null && lines.length > 0) {
|
||||
ret.push({ start, end, name: lines.join('\r\n'), tags: { index: subtitleIndexAt } });
|
||||
}
|
||||
start = undefined;
|
||||
end = undefined;
|
||||
subtitleIndexAt = undefined;
|
||||
lines = [];
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const lineRaw of text.trim().split('\r\n')) {
|
||||
const line = lineRaw.trim();
|
||||
if (line === '') {
|
||||
flush();
|
||||
} else if (subtitleIndexAt != null && subtitleIndexAt > 0) {
|
||||
const match = line.match(/^(\d+:\d+:\d+[,.]\d+\s+)-->(\s+\d+:\d+:\d+[,.]\d+)$/);
|
||||
if (match) {
|
||||
const fixComma = (v) => v.replace(/,/g, '.');
|
||||
start = parseTime(fixComma(match[1]))?.time;
|
||||
end = parseTime(fixComma(match[2]))?.time;
|
||||
} else if (start != null && end != null) {
|
||||
lines.push(line);
|
||||
}
|
||||
} else if (/^\d+$/.test(line)) {
|
||||
const parsedIndex = parseInt(line, 10);
|
||||
if (!Number.isNaN(parsedIndex) && parsedIndex > 0) {
|
||||
subtitleIndexAt = parsedIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flush();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function formatSrt(segments) {
|
||||
return segments.reduce((acc, segment, index) => `${acc}${index > 0 ? '\r\n' : ''}${index + 1}\r\n${formatDuration({ seconds: segment.start }).replace(/\./g, ',')} --> ${formatDuration({ seconds: segment.end }).replace(/\./g, ',')}\r\n${segment.name || '-'}\r\n`, '');
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
|
|||
import { it, describe, expect } from 'vitest';
|
||||
|
||||
|
||||
import { parseYouTube, formatYouTube, parseMplayerEdl, parseXmeml, parseFcpXml, parseCsv, parseCsvTime, getFrameValParser, formatCsvFrames, getFrameCountRaw, parsePbf, parseDvAnalyzerSummaryTxt } from './edlFormats';
|
||||
import { parseSrt, formatSrt, parseYouTube, formatYouTube, parseMplayerEdl, parseXmeml, parseFcpXml, parseCsv, parseCsvTime, getFrameValParser, formatCsvFrames, getFrameCountRaw, parsePbf, parseDvAnalyzerSummaryTxt } from './edlFormats';
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
@ -248,6 +248,14 @@ it('parses pbf', async () => {
|
|||
expect(parsePbf(await readFixture('potplayer bookmark format utf16le issue 867.pbf', null))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('parses srt', async () => {
|
||||
expect(parseSrt(await readFixture('sample.srt'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('format srt', async () => {
|
||||
expect(formatSrt(parseSrt(await readFixture('sample.srt')))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// https://github.com/mifi/lossless-cut/issues/1664
|
||||
it('parses DV Analyzer Summary.txt', async () => {
|
||||
expect(parseDvAnalyzerSummaryTxt(await readFixture('DV Analyzer Summary.txt', 'utf-8'))).toMatchSnapshot();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import JSON5 from 'json5';
|
||||
import i18n from 'i18next';
|
||||
|
||||
import { parseCuesheet, parseXmeml, parseFcpXml, parseCsv, parsePbf, parseMplayerEdl, formatCsvHuman, formatTsv, formatCsvFrames, formatCsvSeconds, parseCsvTime, getFrameValParser, parseDvAnalyzerSummaryTxt } from './edlFormats';
|
||||
import { parseSrt, formatSrt, parseCuesheet, parseXmeml, parseFcpXml, parseCsv, parsePbf, parseMplayerEdl, formatCsvHuman, formatTsv, formatCsvFrames, formatCsvSeconds, parseCsvTime, getFrameValParser, parseDvAnalyzerSummaryTxt } from './edlFormats';
|
||||
import { askForYouTubeInput, showOpenDialog } from './dialogs';
|
||||
import { getOutPath } from './util';
|
||||
|
||||
|
@ -44,6 +44,10 @@ export async function loadCue(path) {
|
|||
return parseCuesheet(cueParser.parse(path));
|
||||
}
|
||||
|
||||
export async function loadSrt(path) {
|
||||
return parseSrt(await fs.readFile(path, 'utf-8'));
|
||||
}
|
||||
|
||||
export async function saveCsv(path, cutSegments) {
|
||||
await fs.writeFile(path, await formatCsvSeconds(cutSegments));
|
||||
}
|
||||
|
@ -60,6 +64,11 @@ export async function saveTsv(path, cutSegments) {
|
|||
await fs.writeFile(path, await formatTsv(cutSegments));
|
||||
}
|
||||
|
||||
export async function saveSrt(path, cutSegments) {
|
||||
await fs.writeFile(path, await formatSrt(cutSegments));
|
||||
}
|
||||
|
||||
|
||||
export async function saveLlcProject({ savePath, filePath, cutSegments }) {
|
||||
const projectData = {
|
||||
version: 1,
|
||||
|
@ -83,6 +92,7 @@ export async function readEdlFile({ type, path, fps }) {
|
|||
if (type === 'cue') return loadCue(path);
|
||||
if (type === 'pbf') return loadPbf(path);
|
||||
if (type === 'mplayer') return loadMplayerEdl(path);
|
||||
if (type === 'srt') return loadSrt(path);
|
||||
if (type === 'llc') {
|
||||
const project = await loadLlcProject(path);
|
||||
return project.cutSegments;
|
||||
|
@ -101,6 +111,7 @@ export async function askForEdlImport({ type, fps }) {
|
|||
else if (type === 'pbf') filters = [{ name: i18n.t('PBF files'), extensions: ['pbf'] }];
|
||||
else if (type === 'mplayer') filters = [{ name: i18n.t('MPlayer EDL'), extensions: ['*'] }];
|
||||
else if (type === 'dv-analyzer-summary-txt') filters = [{ name: i18n.t('DV Analyzer Summary.txt'), extensions: ['txt'] }];
|
||||
else if (type === 'srt') filters = [{ name: i18n.t('Subtitles (SRT)'), extensions: ['srt'] }];
|
||||
else if (type === 'llc') filters = [{ name: i18n.t('LosslessCut project'), extensions: ['llc'] }];
|
||||
|
||||
const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile'], filters });
|
||||
|
@ -123,6 +134,9 @@ export async function exportEdlFile({ type, cutSegments, customOutDir, filePath,
|
|||
} else if (type === 'csv-frames') {
|
||||
ext = 'csv';
|
||||
filters = [{ name: i18n.t('TXT files'), extensions: [ext, 'txt'] }];
|
||||
} else if (type === 'srt') {
|
||||
ext = 'srt';
|
||||
filters = [{ name: i18n.t('Subtitles (SRT)'), extensions: [ext, 'txt'] }];
|
||||
} else if (type === 'llc') {
|
||||
ext = 'llc';
|
||||
filters = [{ name: i18n.t('LosslessCut project'), extensions: [ext, 'llc'] }];
|
||||
|
@ -138,4 +152,5 @@ export async function exportEdlFile({ type, cutSegments, customOutDir, filePath,
|
|||
else if (type === 'csv-human') await saveCsvHuman(savePath, cutSegments);
|
||||
else if (type === 'csv-frames') await saveCsvFrames({ path: savePath, cutSegments, getFrameCount });
|
||||
else if (type === 'llc') await saveLlcProject({ savePath, filePath, cutSegments });
|
||||
else if (type === 'srt') await saveSrt(savePath, cutSegments);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
1
|
||||
00:00:02,000 --> 00:00:06,000
|
||||
First subtitle
|
||||
|
||||
2
|
||||
00:00:28,967 --> 01:30:30.95
|
||||
Subtitle 2 line 1
|
||||
Subtitle 2 line 2
|
Ładowanie…
Reference in New Issue