1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-25 11:43:17 +01:00

reuse time parsing

and make it more flexible
This commit is contained in:
Mikael Finstad 2023-12-02 23:37:56 +08:00
parent 556f8a09e3
commit 233c5c017e
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
3 changed files with 75 additions and 52 deletions

View File

@ -19,28 +19,42 @@ export function getFrameCountRaw(detectedFps, sec) {
return Math.round(sec * detectedFps); return Math.round(sec * detectedFps);
} }
export async function parseCsv(csvStr, processTime = (t) => t) { function parseTime(str) {
const timeMatch = str.match(/^[^0-9]*(?:(?:([0-9]{1,}):)?([0-9]{1,2}):)?([0-9]{1,})(?:\.([0-9]{1,3}))?:?/);
if (!timeMatch) return undefined;
const rest = str.substring(timeMatch[0].length);
const [, hourStr, minStr, secStr, msStr] = timeMatch;
const hour = hourStr != null ? parseInt(hourStr, 10) : 0;
const min = minStr != null ? parseInt(minStr, 10) : 0;
const sec = parseFloat(msStr != null ? `${secStr}.${msStr}` : secStr);
const time = (((hour * 60) + min) * 60 + sec);
return { time, rest };
}
export function parseCsvTime(str) {
const parsed = parseTime(str.trim());
return parsed?.time;
}
export const getFrameValParser = (fps) => (str) => {
if (str === '') return undefined;
const frameCount = parseFloat(str);
return getTimeFromFrameNum(fps, frameCount);
};
export async function parseCsv(csvStr, parseTimeFn) {
const rows = await csvParseAsync(csvStr, {}); const rows = await csvParseAsync(csvStr, {});
if (rows.length === 0) throw new Error(i18n.t('No rows found')); if (rows.length === 0) throw new Error(i18n.t('No rows found'));
if (!rows.every(row => row.length === 3)) throw new Error(i18n.t('One or more rows does not have 3 columns')); if (!rows.every(row => row.length === 3)) throw new Error(i18n.t('One or more rows does not have 3 columns'));
function parseTimeVal(str) {
if (str === '') return undefined;
let timestampMatch = str.match(/^(\d{1,2}):(\d{1,2}):(\d{1,2})(?:\.(\d{1,3}))?$/);
let parsed = undefined;
if (timestampMatch && timestampMatch.length === 5) {
let [, h, m, s, ms] = timestampMatch;
parsed = parseInt(h, 10) * 60 + parseInt(m, 10) * 60 + parseInt(s, 10) + parseInt(ms, 10) / 1000;
} else {
parsed = parseFloat(str, 10);
}
return processTime(parsed);
}
const mapped = rows const mapped = rows
.map(([start, end, name]) => ({ .map(([start, end, name]) => ({
start: parseTimeVal(start), start: parseTimeFn(start),
end: parseTimeVal(end), end: parseTimeFn(end),
name, name: name.trim(),
})); }));
if (!mapped.every(({ start, end }) => ( if (!mapped.every(({ start, end }) => (
@ -92,7 +106,7 @@ export function parseCuesheet(cuesheet) {
const { tracks } = cuesheet.files[0]; const { tracks } = cuesheet.files[0];
function parseTime(track) { function getTime(track) {
const index = track.indexes[0]; const index = track.indexes[0];
if (!index) return undefined; if (!index) return undefined;
const { time } = index; const { time } = index;
@ -103,9 +117,9 @@ export function parseCuesheet(cuesheet) {
return tracks.map((track, i) => { return tracks.map((track, i) => {
const nextTrack = tracks[i + 1]; const nextTrack = tracks[i + 1];
const end = nextTrack && parseTime(nextTrack); const end = nextTrack && getTime(nextTrack);
return { name: track.title, start: parseTime(track), end, tags: { performer: track.performer, title: track.title } }; return { name: track.title, start: getTime(track), end, tags: { performer: track.performer, title: track.title } };
}); });
} }
@ -168,37 +182,36 @@ export function parseFcpXml(xmlStr) {
const { fcpxml } = xml; const { fcpxml } = xml;
if (!fcpxml) throw Error('Root element <fcpxml> not found in file'); if (!fcpxml) throw Error('Root element <fcpxml> not found in file');
function parseTime(str) { function getTime(str) {
const match = str.match(/([0-9]+)\/([0-9]+)s/); const match = str.match(/([0-9]+)\/([0-9]+)s/);
if (!match) throw new Error('Invalid attribute'); if (!match) throw new Error('Invalid attribute');
return parseInt(match[1], 10) / parseInt(match[2], 10); return parseInt(match[1], 10) / parseInt(match[2], 10);
} }
return fcpxml.library.event.project.sequence.spine['asset-clip'].map((assetClip) => { return fcpxml.library.event.project.sequence.spine['asset-clip'].map((assetClip) => {
const start = parseTime(assetClip['@_start']); const start = getTime(assetClip['@_start']);
const duration = parseTime(assetClip['@_duration']); const duration = getTime(assetClip['@_duration']);
const end = start + duration; const end = start + duration;
return { start, end }; return { start, end };
}); });
} }
export function parseYouTube(str) {
function parseLine(match) {
if (!match) return undefined;
const [, hourStr, minStr, secStr, msStr, name] = match;
const hour = hourStr != null ? parseInt(hourStr, 10) : 0;
const min = parseInt(minStr, 10);
const sec = parseInt(secStr, 10);
const ms = msStr != null ? parseInt(msStr, 10) : 0;
const time = (((hour * 60) + min) * 60 + sec) + ms / 1000; export function parseYouTube(str) {
function parseLine(lineStr) {
const timeParsed = parseTime(lineStr);
if (timeParsed == null) return undefined;
const { time, rest } = timeParsed;
const nameMatch = rest.match(/^[\s-]+([^\n]*)$/);
if (!nameMatch) return undefined;
const [, name] = nameMatch;
return { time, name }; return { time, name };
} }
const lines = str.split('\n').map((lineStr) => { const lines = str.split('\n').map(parseLine).filter((line) => line);
const match = lineStr.match(/^[^0-9]*(?:([0-9]{1,}):)?([0-9]{1,2}):([0-9]{1,2})(?:\.([0-9]{3}))?:?[\s-]+([^\n]*)$/);
return parseLine(match);
}).filter((line) => line);
const linesSorted = sortBy(lines, (l) => l.time); const linesSorted = sortBy(lines, (l) => l.time);

View File

@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
import { it, describe, expect } from 'vitest'; import { it, describe, expect } from 'vitest';
import { parseYouTube, formatYouTube, parseMplayerEdl, parseXmeml, parseFcpXml, parseCsv, getTimeFromFrameNum, formatCsvFrames, formatCsvHuman, getFrameCountRaw, parsePbf, parseDvAnalyzerSummaryTxt } from './edlFormats'; import { parseYouTube, formatYouTube, parseMplayerEdl, parseXmeml, parseFcpXml, parseCsv, parseCsvTime, getFrameValParser, formatCsvFrames, getFrameCountRaw, parsePbf, parseDvAnalyzerSummaryTxt } from './edlFormats';
// eslint-disable-next-line no-underscore-dangle // eslint-disable-next-line no-underscore-dangle
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
@ -16,10 +16,13 @@ const expectYouTube1 = [
{ start: 1, end: 2, name: '"Test 2":' }, { start: 1, end: 2, name: '"Test 2":' },
{ start: 2, end: 4, name: '00:57 double' }, { start: 2, end: 4, name: '00:57 double' },
{ start: 4, end: 5, name: '' }, { start: 4, end: 5, name: '' },
{ start: 5, end: 61, name: '' }, { start: 5, end: 6, name: '' },
{ start: 6, end: 6.01, name: '6 label' },
{ start: 6.01, end: 61, name: '6.01 label' },
{ start: 61, end: 61.012, name: 'Test 3' }, { start: 61, end: 61.012, name: 'Test 3' },
{ start: 61.012, end: 62.012, name: 'Test 6' }, { start: 61.012, end: 62.012, name: 'Test 6' },
{ start: 62.012, end: 3661.012, name: 'Test 7' }, { start: 62.012, end: 132, name: 'Test 7' },
{ start: 132, end: 3661.012, name: 'Integer' },
{ start: 3661.012, end: 10074, name: 'Test - 4' }, { start: 3661.012, end: 10074, name: 'Test - 4' },
{ start: 10074, end: undefined, name: 'Short - hour and hyphen' }, { start: 10074, end: undefined, name: 'Short - hour and hyphen' },
]; ];
@ -40,10 +43,12 @@ describe('parseYouTube', () => {
00:57:01.0123 Invalid 2 00:57:01.0123 Invalid 2
00:57:01. Invalid 3 00:57:01. Invalid 3
01:15:: Invalid 4 01:15:: Invalid 4
0132 Invalid 5 0132 Integer
00:03 00:03
00:04 00:04
00:05 00:05
6 6 label
6.01 6.01 label
`; `;
const edl = parseYouTube(str); const edl = parseYouTube(str);
expect(edl).toEqual(expectYouTube1); expect(edl).toEqual(expectYouTube1);
@ -91,9 +96,12 @@ it('formatYouTube 2', () => {
'0:02 00:57 double', '0:02 00:57 double',
'0:04', '0:04',
'0:05', '0:05',
'0:06 6 label',
'0:06 6.01 label',
'1:01 Test 3', '1:01 Test 3',
'1:01 Test 6', '1:01 Test 6',
'1:02 Test 7', '1:02 Test 7',
'2:12 Integer',
'1:01:01 Test - 4', '1:01:01 Test - 4',
'2:47:54 Short - hour and hyphen', '2:47:54 Short - hour and hyphen',
]); ]);
@ -195,7 +203,7 @@ const csvFramesStr = `\
it('parses csv with frames', async () => { it('parses csv with frames', async () => {
const fps = 30; const fps = 30;
const parsed = await parseCsv(csvFramesStr, (frameCount) => getTimeFromFrameNum(fps, frameCount)); const parsed = await parseCsv(csvFramesStr, getFrameValParser(fps));
expect(parsed).toEqual([ expect(parsed).toEqual([
{ end: 5.166666666666667, name: 'EP106_SQ010_SH0010', start: 0 }, { end: 5.166666666666667, name: 'EP106_SQ010_SH0010', start: 0 },
@ -213,22 +221,24 @@ it('parses csv with frames', async () => {
const csvTimestampStr = `\ const csvTimestampStr = `\
00:01:54.612,00:03:09.053,A 00:01:54.612,00:03:09.053,A
00:05:00.448,00:07:56.194,B 00:05:00.448,00:07:56.194,B
00:09:27.075,00:11:44.264,C 00:09:27.075,00:11:44.264,C
0,1,D
1.01,1.99,E
0:2,0:3,F
`; `;
it('parses csv with timestamps', async () => { it('parses csv with timestamps', async () => {
const fps = 30; const parsed = await parseCsv(csvTimestampStr, parseCsvTime);
const parsed = await parseCsv(csvTimestampStr);
expect(parsed).toEqual([ expect(parsed).toEqual([
{ end: 189.053, name: 'A', start: 114.612}, { end: 189.053, name: 'A', start: 114.612 },
{ end: 476.194, name: 'B', start: 300.448}, { end: 476.194, name: 'B', start: 300.448 },
{ end: 704.264, name: 'C', start: 567.075}, { end: 704.264, name: 'C', start: 567.075 },
{ end: 1, name: 'D', start: 0 },
{ end: 1.99, name: 'E', start: 1.01 },
{ start: 2, name: 'F', end: 3 },
]); ]);
const formatted = await formatCsvHuman(parsed);
expect(formatted).toEqual(csvTimestampStr);
}); });
it('parses pbf', async () => { it('parses pbf', async () => {

View File

@ -1,7 +1,7 @@
import JSON5 from 'json5'; import JSON5 from 'json5';
import i18n from 'i18next'; import i18n from 'i18next';
import { parseCuesheet, parseXmeml, parseFcpXml, parseCsv, parsePbf, parseMplayerEdl, formatCsvHuman, formatTsv, formatCsvFrames, formatCsvSeconds, getTimeFromFrameNum, parseDvAnalyzerSummaryTxt } from './edlFormats'; import { parseCuesheet, parseXmeml, parseFcpXml, parseCsv, parsePbf, parseMplayerEdl, formatCsvHuman, formatTsv, formatCsvFrames, formatCsvSeconds, parseCsvTime, getFrameValParser, parseDvAnalyzerSummaryTxt } from './edlFormats';
import { askForYouTubeInput, showOpenDialog } from './dialogs'; import { askForYouTubeInput, showOpenDialog } from './dialogs';
import { getOutPath } from './util'; import { getOutPath } from './util';
@ -12,12 +12,12 @@ const { basename } = window.require('path');
const { dialog } = window.require('@electron/remote'); const { dialog } = window.require('@electron/remote');
export async function loadCsvSeconds(path) { export async function loadCsvSeconds(path) {
return parseCsv(await fs.readFile(path, 'utf-8')); return parseCsv(await fs.readFile(path, 'utf-8'), parseCsvTime);
} }
export async function loadCsvFrames(path, fps) { export async function loadCsvFrames(path, fps) {
if (!fps) throw new Error('The loaded file has an unknown framerate'); if (!fps) throw new Error('The loaded file has an unknown framerate');
return parseCsv(await fs.readFile(path, 'utf-8'), (frameNum) => getTimeFromFrameNum(fps, frameNum)); return parseCsv(await fs.readFile(path, 'utf-8'), getFrameValParser(fps));
} }
export async function loadXmeml(path) { export async function loadXmeml(path) {