implement import csv with frame numbers #1024

pull/982/head
Mikael Finstad 2022-02-14 12:08:57 +08:00
rodzic 3b3444fc7e
commit fe88b3d6f6
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 25AB36E3E81CBC26
5 zmienionych plików z 107 dodań i 50 usunięć

Wyświetl plik

@ -52,11 +52,17 @@ module.exports = (app, mainWindow, newVersion) => {
label: i18n.t('Import project'),
submenu: [
{
label: i18n.t('LosslessCut (CSV)'),
label: i18n.t('Times in seconds (CSV)'),
click() {
mainWindow.webContents.send('importEdlFile', 'csv');
},
},
{
label: i18n.t('Frame numbers (CSV)'),
click() {
mainWindow.webContents.send('importEdlFile', 'csv-frames');
},
},
{
label: i18n.t('EDL (MPlayer)'),
click() {
@ -93,7 +99,7 @@ module.exports = (app, mainWindow, newVersion) => {
label: i18n.t('Export project'),
submenu: [
{
label: i18n.t('LosslessCut (CSV)'),
label: i18n.t('Times in seconds (CSV)'),
click() {
mainWindow.webContents.send('exportEdlFile', 'csv');
},

Wyświetl plik

@ -53,7 +53,7 @@ import {
getDuration, getTimecodeFromStreams, createChaptersFromSegments, extractSubtitleTrack,
} from './ffmpeg';
import { exportEdlFile, readEdlFile, saveLlcProject, loadLlcProject, askForEdlImport } from './edlStore';
import { formatYouTube } from './edlFormats';
import { formatYouTube, getTimeFromFrameNum as getTimeFromFrameNumRaw, getFrameCountRaw } from './edlFormats';
import {
getOutPath, toast, errorToast, handleError, setFileNameTitle, getOutDir, withBlur,
checkDirWriteAccess, dirExists, openDirToast, isMasBuild, isStoreBuild, dragPreventer, doesPlayerSupportFile,
@ -446,10 +446,9 @@ const App = memo(() => {
setCutSegments(sortBy(cutSegments, getSegApparentStart));
}, [cutSegments, setCutSegments]);
const getFrameCount = useCallback((sec) => {
if (detectedFps == null) return undefined;
return Math.floor(sec * detectedFps);
}, [detectedFps]);
const getFrameCount = useCallback((sec) => getFrameCountRaw(detectedFps, sec), [detectedFps]);
const getTimeFromFrameNum = useCallback((frameNum) => getTimeFromFrameNumRaw(detectedFps, frameNum), [detectedFps]);
const formatTimecode = useCallback(({ seconds, shorten }) => {
if (timecodeFormat === 'frameCount') {
@ -1129,7 +1128,6 @@ const App = memo(() => {
// Emulate a single segment with no cuts (full timeline)
segmentsToExport = [{ start: 0, end: getSegApparentEnd({}) }];
chaptersToAdd = sortBy(enabledOutSegments, 'start').map((segment) => ({ start: segment.start, end: segment.end, name: segment.name }));
console.log(chaptersToAdd);
}
console.log('outSegTemplateOrDefault', outSegTemplateOrDefault);
@ -1911,7 +1909,7 @@ const App = memo(() => {
if (!checkFileOpened()) return;
try {
const edl = await askForEdlImport(type);
const edl = await askForEdlImport({ type, getTimeFromFrameNum });
if (edl.length > 0) loadCutSegments(edl, true);
} catch (err) {
handleError(err);
@ -2049,7 +2047,7 @@ const App = memo(() => {
outputDir, filePath, customOutDir, startTimeOffset, userHtml5ifyCurrentFile, batchLoadPaths,
extractAllStreams, userOpenFiles, openSendReportDialogWithState, setWorking,
loadEdlFile, cutSegments, apparentCutSegments, edlFilePath, toggleHelp, toggleSettings, ensureOutDirAccessible, html5ifyAndLoad, html5ify,
loadCutSegments, duration, checkFileOpened, loadMedia, fileFormat, reorderSegsByStartTime, closeFileWithConfirm, closeBatch, clearSegments, clearSegCounter, fixInvalidDuration, invertAllCutSegments, getFrameCount,
loadCutSegments, duration, checkFileOpened, loadMedia, fileFormat, reorderSegsByStartTime, closeFileWithConfirm, closeBatch, clearSegments, clearSegCounter, fixInvalidDuration, invertAllCutSegments, getFrameCount, getTimeFromFrameNum,
]);
const showAddStreamSourceDialog = useCallback(async () => {

Wyświetl plik

@ -2,6 +2,7 @@ import fastXmlParser from 'fast-xml-parser';
import i18n from 'i18next';
import csvParse from 'csv-parse/lib/browser';
import csvStringify from 'csv-stringify/lib/browser';
import pify from 'pify';
import sortBy from 'lodash/sortBy';
@ -10,16 +11,29 @@ import { formatDuration } from './util/duration';
import { invertSegments, sortSegments } from './segments';
const csvParseAsync = pify(csvParse);
const csvStringifyAsync = pify(csvStringify);
export async function parseCsv(str) {
const rows = await csvParseAsync(str, {});
export const getTimeFromFrameNum = (detectedFps, frameNum) => frameNum / detectedFps;
export function getFrameCountRaw(detectedFps, sec) {
if (detectedFps == null) return undefined;
return Math.floor(sec * detectedFps);
}
export async function parseCsv(csvStr, processTime = (t) => t) {
const rows = await csvParseAsync(csvStr, {});
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'));
function parseTimeVal(str) {
if (str === '') return undefined;
const parsed = parseFloat(str, 10);
return processTime(parsed);
}
const mapped = rows
.map(([start, end, name]) => ({
start: start === '' ? undefined : parseFloat(start, 10),
end: end === '' ? undefined : parseFloat(end, 10),
start: parseTimeVal(start),
end: parseTimeVal(end),
name,
}));
@ -168,3 +182,35 @@ export function formatYouTube(segments) {
return `${timeStr}${namePart}`;
}).join('\n');
}
const safeFormatDuration = (duration) => (duration != null ? formatDuration({ seconds: duration }) : '');
const safeFormatFrameCount = ({ seconds, getFrameCount }) => (seconds != null ? getFrameCount(seconds) : '');
export const formatSegmentsTimes = (cutSegments) => cutSegments.map(({ start, end, name }) => [
safeFormatDuration(start),
safeFormatDuration(end),
name,
]);
const formatSegmentsFrameCounts = ({ cutSegments, getFrameCount }) => cutSegments.map(({ start, end, name }) => [
safeFormatFrameCount({ seconds: start, getFrameCount }),
safeFormatFrameCount({ seconds: end, getFrameCount }),
name,
]);
export async function formatCsvFrames({ cutSegments, getFrameCount }) {
return csvStringifyAsync(formatSegmentsFrameCounts({ cutSegments, getFrameCount }));
}
export async function formatCsvSeconds(cutSegments) {
const rows = cutSegments.map(({ start, end, name }) => [start, end, name]);
return csvStringifyAsync(rows);
}
export async function formatCsvHuman(cutSegments) {
return csvStringifyAsync(formatSegmentsTimes(cutSegments));
}
export async function formatTsv(cutSegments) {
return csvStringifyAsync(formatSegmentsTimes(cutSegments), { delimiter: '\t' });
}

Wyświetl plik

@ -1,7 +1,7 @@
import fs from 'fs';
import { join } from 'path';
import { parseYouTube, formatYouTube, parseMplayerEdl, parseXmeml } from './edlFormats';
import { parseYouTube, formatYouTube, parseMplayerEdl, parseXmeml, parseCsv, getTimeFromFrameNum, formatCsvFrames, getFrameCountRaw } from './edlFormats';
const readFixture = async (name, encoding = 'utf-8') => fs.promises.readFile(join(__dirname, 'fixtures', name), encoding);
@ -139,3 +139,29 @@ it('parses xmeml 1', async () => {
it('parses xmeml 2', async () => {
expect(await parseXmeml(await readFixture('Final Cut Pro XMEML 2.xml'))).toMatchSnapshot();
});
// https://github.com/mifi/lossless-cut/issues/1024
const csvFramesStr = `\
0,155,EP106_SQ010_SH0010
156,251,EP106_SQ010_SH0020
252,687,EP106_SQ010_SH0030
688,747,EP106_SQ020_SH0010
`;
it('parses csv with frames', async () => {
const fps = 30;
const parsed = await parseCsv(csvFramesStr, (frameCount) => getTimeFromFrameNum(fps, frameCount));
expect(parsed).toEqual([
{ end: 5.166666666666667, name: 'EP106_SQ010_SH0010', start: 0 },
{ end: 8.366666666666667, name: 'EP106_SQ010_SH0020', start: 5.2 },
{ end: 22.9, name: 'EP106_SQ010_SH0030', start: 8.4 },
{ end: 24.9, name: 'EP106_SQ020_SH0010', start: 22.933333333333334 },
]);
const formatted = await formatCsvFrames({
cutSegments: parsed,
getFrameCount: (sec) => getFrameCountRaw(fps, sec),
});
expect(formatted).toEqual(csvFramesStr);
});

Wyświetl plik

@ -1,10 +1,7 @@
import csvStringify from 'csv-stringify/lib/browser';
import pify from 'pify';
import JSON5 from 'json5';
import i18n from 'i18next';
import { parseCuesheet, parseXmeml, parseCsv, parsePbf, parseMplayerEdl } from './edlFormats';
import { formatDuration } from './util/duration';
import { parseCuesheet, parseXmeml, parseCsv, parsePbf, parseMplayerEdl, formatCsvHuman, formatTsv, formatCsvFrames, formatCsvSeconds } from './edlFormats';
import { askForYouTubeInput } from './dialogs';
const fs = window.require('fs-extra');
@ -14,12 +11,14 @@ const { basename } = window.require('path');
const electron = window.require('electron'); // eslint-disable-line
const { dialog } = electron.remote;
const csvStringifyAsync = pify(csvStringify);
export async function loadCsv(path) {
export async function loadCsvSeconds(path) {
return parseCsv(await fs.readFile(path, 'utf-8'));
}
export async function loadCsvFrames(path, getTimeFromFrameNum) {
return parseCsv(await fs.readFile(path, 'utf-8'), (frameNum) => getTimeFromFrameNum(frameNum));
}
export async function loadXmeml(path) {
return parseXmeml(await fs.readFile(path, 'utf-8'));
}
@ -37,38 +36,19 @@ export async function loadCue(path) {
}
export async function saveCsv(path, cutSegments) {
const rows = cutSegments.map(({ start, end, name }) => [start, end, name]);
const str = await csvStringifyAsync(rows);
await fs.writeFile(path, str);
await fs.writeFile(path, await formatCsvSeconds(cutSegments));
}
const safeFormatDuration = (duration) => (duration != null ? formatDuration({ seconds: duration }) : '');
const safeFormatFrameCount = ({ seconds, getFrameCount }) => (seconds != null ? getFrameCount(seconds) : '');
const formatSegmentsTimes = (cutSegments) => cutSegments.map(({ start, end, name }) => [
safeFormatDuration(start),
safeFormatDuration(end),
name,
]);
const formatSegmentsFrameCounts = ({ cutSegments, getFrameCount }) => cutSegments.map(({ start, end, name }) => [
safeFormatFrameCount({ seconds: start, getFrameCount }),
safeFormatFrameCount({ seconds: end, getFrameCount }),
name,
]);
export async function saveCsvHuman(path, cutSegments) {
const str = await csvStringifyAsync(formatSegmentsTimes(cutSegments));
await fs.writeFile(path, str);
await fs.writeFile(path, await formatCsvHuman(cutSegments));
}
export async function saveCsvFrames({ path, cutSegments, getFrameCount }) {
const str = await csvStringifyAsync(formatSegmentsFrameCounts({ cutSegments, getFrameCount }));
await fs.writeFile(path, str);
await fs.writeFile(path, await formatCsvFrames({ cutSegments, getFrameCount }));
}
export async function saveTsv(path, cutSegments) {
const str = await csvStringifyAsync(formatSegmentsTimes(cutSegments), { delimiter: '\t' });
await fs.writeFile(path, str);
await fs.writeFile(path, await formatTsv(cutSegments));
}
export async function saveLlcProject({ savePath, filePath, cutSegments }) {
@ -84,8 +64,9 @@ export async function loadLlcProject(path) {
return JSON5.parse(await fs.readFile(path));
}
export async function readEdlFile({ type, path }) {
if (type === 'csv') return loadCsv(path);
export async function readEdlFile({ type, path, getTimeFromFrameNum }) {
if (type === 'csv') return loadCsvSeconds(path);
if (type === 'csv-frames') return loadCsvFrames(path, getTimeFromFrameNum);
if (type === 'xmeml') return loadXmeml(path);
if (type === 'cue') return loadCue(path);
if (type === 'pbf') return loadPbf(path);
@ -97,11 +78,11 @@ export async function readEdlFile({ type, path }) {
throw new Error('Invalid EDL type');
}
export async function askForEdlImport(type) {
export async function askForEdlImport({ type, getTimeFromFrameNum }) {
if (type === 'youtube') return askForYouTubeInput();
let filters;
if (type === 'csv') filters = [{ name: i18n.t('CSV files'), extensions: ['csv'] }];
if (type === 'csv' || type === 'csv-frames') filters = [{ name: i18n.t('CSV files'), extensions: ['csv'] }];
else if (type === 'xmeml') filters = [{ name: i18n.t('XML files'), extensions: ['xml'] }];
else if (type === 'cue') filters = [{ name: i18n.t('CUE files'), extensions: ['cue'] }];
else if (type === 'pbf') filters = [{ name: i18n.t('PBF files'), extensions: ['pbf'] }];
@ -110,7 +91,7 @@ export async function askForEdlImport(type) {
const { canceled, filePaths } = await dialog.showOpenDialog({ properties: ['openFile'], filters });
if (canceled || filePaths.length < 1) return [];
return readEdlFile({ type, path: filePaths[0] });
return readEdlFile({ type, path: filePaths[0], getTimeFromFrameNum });
}
export async function exportEdlFile({ type, cutSegments, filePath, getFrameCount }) {