From 3cc997bcb2649967cbff76e838f0e798d8289240 Mon Sep 17 00:00:00 2001 From: Lars-Olof Kreim <> Date: Sun, 7 Apr 2024 21:46:05 +0200 Subject: [PATCH 1/2] Add cutlist import option --- src/main/menu.ts | 6 +++ src/renderer/src/edlFormats.test.ts | 58 ++++++++++++++++++++++++- src/renderer/src/edlFormats.ts | 66 +++++++++++++++++++++++++++++ src/renderer/src/edlStore.ts | 7 ++- src/renderer/src/types.ts | 2 +- 5 files changed, 136 insertions(+), 3 deletions(-) diff --git a/src/main/menu.ts b/src/main/menu.ts index 938c413..8ee3aa2 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -69,6 +69,12 @@ export default ({ app, mainWindow, newVersion, isStoreBuild }: { mainWindow.webContents.send('importEdlFile', 'csv-frames'); }, }, + { + label: esc(t('Cutlist')), + click() { + mainWindow.webContents.send('importEdlFile', 'cutlist'); + }, + }, { label: esc(t('EDL (MPlayer)')), click() { diff --git a/src/renderer/src/edlFormats.test.ts b/src/renderer/src/edlFormats.test.ts index c3c4938..d474837 100644 --- a/src/renderer/src/edlFormats.test.ts +++ b/src/renderer/src/edlFormats.test.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from 'url'; import { it, describe, expect } from 'vitest'; -import { parseSrt, formatSrt, 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, parseCutlist } from './edlFormats'; // eslint-disable-next-line no-underscore-dangle const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -242,6 +242,62 @@ it('parses csv with timestamps', async () => { ]); }); +const cutlistStr = ` +[General] +Application=SomeApplication.exe +Version=0.0.0.1 +FramesPerSecond=25 +IntendedCutApplicationName=SomeApplication +IntendedCutApplication=SomeApplication.exe +VDUseSmartRendering=1 +IntendedCutApplicationVersion=1.7.8 +comment1=The following parts of the movie will be kept, the rest will be cut out. +comment2=All values are given in seconds. +NoOfCuts=2 +ApplyToFile=Some_File_Name.avi +OriginalFileSizeBytes=123456 + +[Cut0] +Start=849.12 +StartFrame=21228 +Duration=1881.84 +DurationFrames=47046 + +[Cut1] +Start=3147.72 +StartFrame=78693 +Duration=944.6 +DurationFrames=23615 + +[Info] +Author=AuthorName +RatingByAuthor=0 +EPGError=0 +ActualContent= +MissingBeginning=0 +MissingEnding=0 +MissingVideo=0 +MissingAudio=0 +OtherError=0 +OtherErrorDescription= +SuggestedMovieName= +UserComment=cutted with XXXX + +[Meta] +CutlistId=12345 +GeneratedOn=1900-01-01 00:00:01 +GeneratedBy=cutlist v0.0.0 +`; + +it('parses cutlist', async () => { + const parsed = await parseCutlist(cutlistStr); + + expect(parsed).toEqual([ + { end: 2730.96, name: 'Cut 0', start: 849.12 }, + { end: 4092.32, name: 'Cut 1', start: 3147.72 }, + ]); +}); + it('parses pbf', async () => { expect(parsePbf(await readFixtureBinary('test1.pbf'))).toMatchSnapshot(); expect(parsePbf(await readFixtureBinary('test2.pbf'))).toMatchSnapshot(); diff --git a/src/renderer/src/edlFormats.ts b/src/renderer/src/edlFormats.ts index 90d472f..d70327d 100644 --- a/src/renderer/src/edlFormats.ts +++ b/src/renderer/src/edlFormats.ts @@ -70,6 +70,72 @@ export async function parseCsv(csvStr: string, parseTimeFn: (a: string) => numbe return mapped; } +export async function parseCutlist(clStr: string) { + + // first parse INI-File into "iniValue" object + const regex = { + section: /^\s*\[\s*([^\]]*)\s*\]\s*$/, + param: /^\s*([^=]+?)\s*=\s*(.*?)\s*$/, + comment: /^\s*;.*$/ + }; + const iniValue = {}; + const lines = clStr.split(/[\r\n]+/); + let section:string|null|undefined = null; + lines.forEach(function(line){ + if(regex.comment.test(line)){ + return; + }else if(regex.param.test(line)){ + const match = line.match(regex.param) || []; + if(match[1]){ + if(section){ + iniValue[section][match[1]] = match[2]; + }else{ + iniValue[match[1]] = match[2]; + } + } + }else if(regex.section.test(line)){ + const match = line.match(regex.section) || []; + if(match[1]){ + iniValue[match[1]] = {}; + section = match[1]; + } + }else if(line.length == 0 && section){ + section = null; + }; + }); + + // end INI-File parse + + let found = true; + let i = 0; + const cutArr:{start:number, end:number, name:string}[] = []; + while(found) { + const cutEntry = iniValue['Cut'+i]; + if (cutEntry) { + const start = parseFloat(cutEntry.Start); + const end = Math.round((start + parseFloat(cutEntry.Duration) + Number.EPSILON) * 100) / 100 + cutArr.push({ + start: start, + end: end, + name: `Cut ${i}`, + }); + } else { + found = false; + } + i++; + } + + if (!cutArr.every(({ start, end }) => ( + (start === undefined || !Number.isNaN(start)) + && (end === undefined || !Number.isNaN(end)) + ))) { + console.log(cutArr); + throw new Error(i18n.t('Invalid start or end value. Must contain a number of seconds')); + } + + return cutArr; +} + export async function parseMplayerEdl(text: string) { const allRows = text.split('\n').flatMap((line) => { const match = line.match(/^\s*(\S+)\s+(\S+)\s+([0-3])\s*$/); diff --git a/src/renderer/src/edlStore.ts b/src/renderer/src/edlStore.ts index 1194ed5..65642e4 100644 --- a/src/renderer/src/edlStore.ts +++ b/src/renderer/src/edlStore.ts @@ -2,7 +2,7 @@ import JSON5 from 'json5'; import i18n from 'i18next'; import type { parse as CueParse } from 'cue-parser'; -import { parseSrt, formatSrt, parseCuesheet, parseXmeml, parseFcpXml, parseCsv, parsePbf, parseMplayerEdl, formatCsvHuman, formatTsv, formatCsvFrames, formatCsvSeconds, parseCsvTime, getFrameValParser, parseDvAnalyzerSummaryTxt } from './edlFormats'; +import { parseSrt, formatSrt, parseCuesheet, parseXmeml, parseFcpXml, parseCsv, parseCutlist, parsePbf, parseMplayerEdl, formatCsvHuman, formatTsv, formatCsvFrames, formatCsvSeconds, parseCsvTime, getFrameValParser, parseDvAnalyzerSummaryTxt } from './edlFormats'; import { askForYouTubeInput, showOpenDialog } from './dialogs'; import { getOutPath } from './util'; import { EdlExportType, EdlFileType, EdlImportType, Segment, StateSegment } from './types'; @@ -22,6 +22,10 @@ export async function loadCsvFrames(path: string, fps?: number) { return parseCsv(await readFile(path, 'utf8'), getFrameValParser(fps)); } +export async function loadCutlistSeconds(path: string) { + return parseCutlist(await readFile(path, 'utf8')); +} + export async function loadXmeml(path: string) { return parseXmeml(await readFile(path, 'utf8')); } @@ -96,6 +100,7 @@ export async function loadLlcProject(path: string) { export async function readEdlFile({ type, path, fps }: { type: EdlFileType, path: string, fps?: number | undefined }) { if (type === 'csv') return loadCsvSeconds(path); if (type === 'csv-frames') return loadCsvFrames(path, fps); + if (type === 'cutlist') return loadCutlistSeconds(path); if (type === 'xmeml') return loadXmeml(path); if (type === 'fcpxml') return loadFcpXml(path); if (type === 'dv-analyzer-summary-txt') return loadDvAnalyzerSummaryTxt(path); diff --git a/src/renderer/src/types.ts b/src/renderer/src/types.ts index 45e16d7..0b7fbb0 100644 --- a/src/renderer/src/types.ts +++ b/src/renderer/src/types.ts @@ -61,7 +61,7 @@ export interface InverseCutSegment { export type PlaybackMode = 'loop-segment-start-end' | 'loop-segment' | 'play-segment-once' | 'loop-selected-segments'; -export type EdlFileType = 'csv' | 'csv-frames' | 'xmeml' | 'fcpxml' | 'dv-analyzer-summary-txt' | 'cue' | 'pbf' | 'mplayer' | 'srt' | 'llc'; +export type EdlFileType = 'csv' | 'csv-frames' | 'cutlist' | 'xmeml' | 'fcpxml' | 'dv-analyzer-summary-txt' | 'cue' | 'pbf' | 'mplayer' | 'srt' | 'llc'; export type EdlImportType = 'youtube' | EdlFileType; From e52814087f3f9c01f8d275956002779bcb2a76a9 Mon Sep 17 00:00:00 2001 From: Lars-Olof Kreim <> Date: Mon, 8 Apr 2024 22:04:00 +0200 Subject: [PATCH 2/2] fix linting errors --- src/renderer/src/edlFormats.ts | 73 +++++++++++++++++----------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/src/renderer/src/edlFormats.ts b/src/renderer/src/edlFormats.ts index d70327d..0fb06f4 100644 --- a/src/renderer/src/edlFormats.ts +++ b/src/renderer/src/edlFormats.ts @@ -71,37 +71,38 @@ export async function parseCsv(csvStr: string, parseTimeFn: (a: string) => numbe } export async function parseCutlist(clStr: string) { - // first parse INI-File into "iniValue" object const regex = { - section: /^\s*\[\s*([^\]]*)\s*\]\s*$/, + section: /^\s*\[\s*([^\]]*)\s*]\s*$/, param: /^\s*([^=]+?)\s*=\s*(.*?)\s*$/, - comment: /^\s*;.*$/ + comment: /^\s*;.*$/, }; const iniValue = {}; - const lines = clStr.split(/[\r\n]+/); + const lines = clStr.split(/[\n\r]+/); let section:string|null|undefined = null; - lines.forEach(function(line){ - if(regex.comment.test(line)){ - return; - }else if(regex.param.test(line)){ - const match = line.match(regex.param) || []; - if(match[1]){ - if(section){ - iniValue[section][match[1]] = match[2]; - }else{ - iniValue[match[1]] = match[2]; - } - } - }else if(regex.section.test(line)){ - const match = line.match(regex.section) || []; - if(match[1]){ - iniValue[match[1]] = {}; - section = match[1]; - } - }else if(line.length == 0 && section){ - section = null; - }; + lines.forEach((line) => { + if (regex.comment.test(line)) { + return; + } if (regex.param.test(line)) { + const match = line.match(regex.param) || []; + const [, m1, m2] = match; + if (m1) { + if (section) { + iniValue[section][m1] = m2; + } else { + iniValue[m1] = m2; + } + } + } else if (regex.section.test(line)) { + const match = line.match(regex.section) || []; + const [, m1] = match; + if (m1) { + iniValue[m1] = {}; + section = m1; + } + } else if (line.length === 0 && section) { + section = null; + } }); // end INI-File parse @@ -109,20 +110,20 @@ export async function parseCutlist(clStr: string) { let found = true; let i = 0; const cutArr:{start:number, end:number, name:string}[] = []; - while(found) { - const cutEntry = iniValue['Cut'+i]; + while (found) { + const cutEntry = iniValue[`Cut${i}`]; if (cutEntry) { - const start = parseFloat(cutEntry.Start); - const end = Math.round((start + parseFloat(cutEntry.Duration) + Number.EPSILON) * 100) / 100 - cutArr.push({ - start: start, - end: end, - name: `Cut ${i}`, - }); + const start = parseFloat(cutEntry.Start); + const end = Math.round((start + parseFloat(cutEntry.Duration) + Number.EPSILON) * 100) / 100; + cutArr.push({ + start, + end, + name: `Cut ${i}`, + }); } else { - found = false; + found = false; } - i++; + i += 1; } if (!cutArr.every(({ start, end }) => (