diff --git a/src/main/menu.ts b/src/main/menu.ts index 938c4130..8ee3aa2a 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 c3c49384..d474837b 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 90d472f9..d70327d6 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 1194ed5c..65642e40 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 45e16d77..0b7fbb0e 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;