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

imrpve types

This commit is contained in:
Mikael Finstad 2024-03-03 01:12:35 +08:00
parent daaa2c652d
commit 2e0b9887fd
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
23 changed files with 216 additions and 138 deletions

View File

@ -49,6 +49,7 @@
"@tsconfig/vite-react": "^3.0.0",
"@types/eslint": "^8",
"@types/lodash": "^4.14.202",
"@types/node": "18",
"@types/sortablejs": "^1.15.0",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",

View File

@ -85,7 +85,7 @@ import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './uti
import BigWaveform from './components/BigWaveform';
import isDev from './isDev';
import { EdlFileType, FfmpegCommandLog, FfprobeChapter, FfprobeFormat, FfprobeStream, Html5ifyMode, PlaybackMode, StateSegment, Thumbnail, TunerType } from './types';
import { EdlFileType, FfmpegCommandLog, FfprobeChapter, FfprobeFormat, FfprobeStream, Html5ifyMode, PlaybackMode, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types';
const electron = window.require('electron');
const { exists } = window.require('fs-extra');
@ -121,7 +121,7 @@ function App() {
const [rotation, setRotation] = useState(360);
const [cutProgress, setCutProgress] = useState<number>();
const [startTimeOffset, setStartTimeOffset] = useState(0);
const [filePath, setFilePath] = useState('');
const [filePath, setFilePath] = useState<string>();
const [externalFilesMeta, setExternalFilesMeta] = useState({});
const [customTagsByFile, setCustomTagsByFile] = useState({});
const [paramsByStreamId, setParamsByStreamId] = useState(new Map());
@ -444,7 +444,7 @@ function App() {
const usingPreviewFile = !!previewFilePath;
const effectiveFilePath = previewFilePath || filePath;
const fileUri = useMemo(() => {
if (!effectiveFilePath) return '';
if (!effectiveFilePath) return ''; // Setting video src="" prevents memory leak in chromium
const uri = filePathToUrl(effectiveFilePath);
// https://github.com/mifi/lossless-cut/issues/1674
if (cacheBuster !== 0) {
@ -458,10 +458,10 @@ function App() {
const projectSuffix = 'proj.llc';
const oldProjectSuffix = 'llc-edl.csv';
// New LLC format can be stored along with input file or in working dir (customOutDir)
const getEdlFilePath = useCallback((fp: string, cod?: string) => getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: projectSuffix }), []);
const getEdlFilePath = useCallback((fp?: string, cod?: string) => getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: projectSuffix }), []);
// Old versions of LosslessCut used CSV files and stored them always in customOutDir:
const getEdlFilePathOld = useCallback((fp, cod) => getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: oldProjectSuffix }), []);
const getProjectFileSavePath = useCallback((storeProjectInWorkingDirIn) => getEdlFilePath(filePath, storeProjectInWorkingDirIn ? customOutDir : undefined), [getEdlFilePath, filePath, customOutDir]);
const getProjectFileSavePath = useCallback((storeProjectInWorkingDirIn: boolean) => getEdlFilePath(filePath, storeProjectInWorkingDirIn ? customOutDir : undefined), [getEdlFilePath, filePath, customOutDir]);
const projectFileSavePath = useMemo(() => getProjectFileSavePath(storeProjectInWorkingDir), [getProjectFileSavePath, storeProjectInWorkingDir]);
const currentSaveOperation = useMemo(() => {
@ -664,7 +664,7 @@ function App() {
const allFilesMeta = useMemo(() => ({
...externalFilesMeta,
[filePath]: mainFileMeta,
...(filePath ? { [filePath]: mainFileMeta } : {}),
}), [externalFilesMeta, filePath, mainFileMeta]);
// total number of streams for ALL files
@ -742,6 +742,7 @@ function App() {
const { waveforms } = useWaveform({ darkMode, filePath, relevantTime, waveformEnabled, audioStream: activeAudioStream, shouldShowWaveform, ffmpegExtractWindow, durationSafe });
const resetMergedOutFileName = useCallback(() => {
if (fileFormat == null || filePath == null) return;
const ext = getOutFileExtension({ isCustomFormatSelected, outFormat: fileFormat, filePath });
const outFileName = getSuffixedFileName(filePath, `cut-merged-${Date.now()}${ext}`);
setMergedOutFileName(outFileName);
@ -770,7 +771,7 @@ function App() {
setRotation(360);
setCutProgress(undefined);
setStartTimeOffset(0);
setFilePath(''); // Setting video src="" prevents memory leak in chromium
setFilePath(undefined);
setExternalFilesMeta({});
setCustomTagsByFile({});
setParamsByStreamId(new Map());
@ -1173,16 +1174,17 @@ function App() {
}
}, [cleanupFilesWithDialog, isFileOpened, setWorking]);
const generateOutSegFileNames = useCallback(({ segments = segmentsToExport, template }) => (
generateOutSegFileNamesRaw({ segments, template, formatTimecode, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding })
), [fileFormat, filePath, formatTimecode, isCustomFormatSelected, maxLabelLength, outputDir, outputFileNameMinZeroPadding, safeOutputFileName, segmentsToExport]);
const generateOutSegFileNames = useCallback(({ segments = segmentsToExport, template }: { segments?: SegmentToExport[], template: string }) => {
if (fileFormat == null || outputDir == null || filePath == null) throw new Error();
return generateOutSegFileNamesRaw({ segments, template, formatTimecode, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding });
}, [fileFormat, filePath, formatTimecode, isCustomFormatSelected, maxLabelLength, outputDir, outputFileNameMinZeroPadding, safeOutputFileName, segmentsToExport]);
const closeExportConfirm = useCallback(() => setExportConfirmVisible(false), []);
const willMerge = segmentsToExport.length > 1 && autoMerge;
const mergedOutFilePath = useMemo(() => (
getOutPath({ customOutDir, filePath, fileName: mergedOutFileName })
mergedOutFileName != null ? getOutPath({ customOutDir, filePath, fileName: mergedOutFileName }) : undefined
), [customOutDir, filePath, mergedOutFileName]);
const onExportConfirm = useCallback(async () => {
@ -1351,6 +1353,7 @@ function App() {
try {
const currentTime = getRelevantTime();
const video = videoRef.current;
if (video == null) throw new Error();
const useFffmpeg = usingPreviewFile || captureFrameMethod === 'ffmpeg';
const outPath = useFffmpeg
? await captureFrameFromFfmpeg({ customOutDir, filePath, fromTime: currentTime, captureFormat, quality: captureFrameQuality })
@ -1376,10 +1379,10 @@ function App() {
setCutProgress(0);
let lastOutPath;
let lastOutPath: string | undefined;
let totalProgress = 0;
const onProgress = (progress) => {
const onProgress = (progress: number) => {
totalProgress += progress;
setCutProgress(totalProgress / segments.length);
};
@ -1387,10 +1390,11 @@ function App() {
// eslint-disable-next-line no-restricted-syntax
for (const segment of segments) {
const { start, end } = segment;
if (filePath == null) throw new Error();
// eslint-disable-next-line no-await-in-loop
lastOutPath = await captureFramesRange({ customOutDir, filePath, fps: detectedFps, fromTime: start, toTime: end, estimatedMaxNumFiles: captureFramesResponse.estimatedMaxNumFiles, captureFormat, quality: captureFrameQuality, filter: captureFramesResponse.filter, outputTimestamps: captureFrameFileNameFormat === 'timestamp', onProgress });
}
if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: lastOutPath, text: i18n.t('Frames extracted to: {{path}}', { path: outputDir }) });
if (!hideAllNotifications && lastOutPath != null) openDirToast({ icon: 'success', filePath: lastOutPath, text: i18n.t('Frames extracted to: {{path}}', { path: outputDir }) });
} catch (err) {
handleError(err);
} finally {
@ -1693,8 +1697,8 @@ function App() {
try {
setWorking({ text: i18n.t('Extracting all streams') });
setStreamsSelectorShown(false);
const extractedPaths = await extractStreams({ customOutDir, filePath, streams: mainCopiedStreams, enableOverwriteOutput });
if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: extractedPaths[0], text: i18n.t('All streams have been extracted as separate files') });
const [firstExtractedPath] = await extractStreams({ customOutDir, filePath, streams: mainCopiedStreams, enableOverwriteOutput });
if (!hideAllNotifications && firstExtractedPath != null) openDirToast({ icon: 'success', filePath: firstExtractedPath, text: i18n.t('All streams have been extracted as separate files') });
} catch (err) {
if (err instanceof RefuseOverwriteError) {
showRefuseToOverwrite();
@ -1982,8 +1986,9 @@ function App() {
const showIncludeExternalStreamsDialog = useCallback(async () => {
try {
const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile'] });
if (canceled || filePaths.length === 0) return;
await addStreamSourceFile(filePaths[0]);
const [firstFilePath] = filePaths;
if (canceled || firstFilePath == null) return;
await addStreamSourceFile(firstFilePath);
} catch (err) {
handleError(err);
}
@ -2008,7 +2013,9 @@ function App() {
const onEditSegmentTags = useCallback((index: number) => {
setEditingSegmentTagsSegmentIndex(index);
setEditingSegmentTags(getSegmentTags(apparentCutSegments[index]));
const seg = apparentCutSegments[index];
if (seg == null) throw new Error();
setEditingSegmentTags(getSegmentTags(seg));
}, [apparentCutSegments]);
const editCurrentSegmentTags = useCallback(() => {
@ -2219,8 +2226,8 @@ function App() {
try {
setWorking({ text: i18n.t('Extracting track') });
// setStreamsSelectorShown(false);
const extractedPaths = await extractStreams({ customOutDir, filePath, streams: mainStreams.filter((s) => s.index === index), enableOverwriteOutput });
if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: extractedPaths[0], text: i18n.t('Track has been extracted') });
const [firstExtractedPath] = await extractStreams({ customOutDir, filePath, streams: mainStreams.filter((s) => s.index === index), enableOverwriteOutput });
if (!hideAllNotifications && firstExtractedPath != null) openDirToast({ icon: 'success', filePath: firstExtractedPath, text: i18n.t('Track has been extracted') });
} catch (err) {
if (err instanceof RefuseOverwriteError) {
showRefuseToOverwrite();

View File

@ -5,8 +5,10 @@ import { FaAngleRight, FaFile } from 'react-icons/fa';
import useContextMenu from '../hooks/useContextMenu';
import { primaryTextColor } from '../colors';
const BatchFile = memo(({ path, isOpen, isSelected, name, onSelect, onDelete }) => {
const ref = useRef();
const BatchFile = memo(({ path, isOpen, isSelected, name, onSelect, onDelete }: {
path: string, isOpen: boolean, isSelected: boolean, name: string, onSelect: (a: string) => void, onDelete: (a: string) => void
}) => {
const ref = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
const contextMenuTemplate = useMemo(() => [

View File

@ -225,7 +225,7 @@ const ConcatDialog = memo(({ isShown, onHide, paths, onConcat, alwaysConcatMulti
<Checkbox checked={preserveMetadataOnMerge} onChange={(e) => setPreserveMetadataOnMerge(e.target.checked)} label={t('Preserve original metadata when merging? (slow)')} />
{isMov(fileFormat) && <Checkbox checked={preserveMovData} onChange={(e) => setPreserveMovData(e.target.checked)} label={t('Preserve all MP4/MOV metadata?')} />}
{fileFormat != null && isMov(fileFormat) && <Checkbox checked={preserveMovData} onChange={(e) => setPreserveMovData(e.target.checked)} label={t('Preserve all MP4/MOV metadata?')} />}
<Checkbox checked={segmentsToChapters} onChange={(e) => setSegmentsToChapters(e.target.checked)} label={t('Create chapters from merged segments? (slow)')} />

View File

@ -1,10 +0,0 @@
import { memo } from 'react';
import { primaryTextColor } from '../colors';
export const highlightedTextStyle = { textDecoration: 'underline', textUnderlineOffset: '.2em', textDecorationColor: primaryTextColor, color: 'var(--gray12)', borderRadius: '.4em' };
// eslint-disable-next-line react/jsx-props-no-spreading
const HighlightedText = memo(({ children, style, ...props }) => <span {...props} style={{ ...highlightedTextStyle, ...style }}>{children}</span>);
export default HighlightedText;

View File

@ -0,0 +1,12 @@
import { CSSProperties, HTMLAttributes, memo } from 'react';
import { primaryTextColor } from '../colors';
export const highlightedTextStyle: CSSProperties = { textDecoration: 'underline', textUnderlineOffset: '.2em', textDecorationColor: primaryTextColor, color: 'var(--gray12)', borderRadius: '.4em' };
function HighlightedText({ children, style, ...props }: HTMLAttributes<HTMLSpanElement>) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <span {...props} style={{ ...highlightedTextStyle, ...style }}>{children}</span>;
}
export default memo(HighlightedText);

View File

@ -9,11 +9,12 @@ import { motion, AnimatePresence } from 'framer-motion';
import Swal from '../swal';
import HighlightedText from './HighlightedText';
import { defaultOutSegTemplate, segNumVariable, segSuffixVariable } from '../util/outputNameTemplate';
import { defaultOutSegTemplate, segNumVariable, segSuffixVariable, generateOutSegFileNames as generateOutSegFileNamesRaw } from '../util/outputNameTemplate';
import useUserSettings from '../hooks/useUserSettings';
import Switch from './Switch';
import Select from './Select';
import TextInput from './TextInput';
import { SegmentToExport } from '../types';
const ReactSwal = withReactContent(Swal);
@ -23,16 +24,18 @@ const formatVariable = (variable) => `\${${variable}}`;
const extVar = formatVariable('EXT');
const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generateOutSegFileNames, currentSegIndexSafe }) => {
const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generateOutSegFileNames, currentSegIndexSafe }: {
outSegTemplate: string, setOutSegTemplate: (text: string) => void, generateOutSegFileNames: (a: { segments?: SegmentToExport[], template: string }) => ReturnType<typeof generateOutSegFileNamesRaw>, currentSegIndexSafe: number,
}) => {
const { safeOutputFileName, toggleSafeOutputFileName, outputFileNameMinZeroPadding, setOutputFileNameMinZeroPadding } = useUserSettings();
const [text, setText] = useState(outSegTemplate);
const [debouncedText] = useDebounce(text, 500);
const [validText, setValidText] = useState();
const [outSegProblems, setOutSegProblems] = useState({ error: undefined, sameAsInputFileNameWarning: false });
const [outSegFileNames, setOutSegFileNames] = useState();
const [shown, setShown] = useState();
const inputRef = useRef();
const [validText, setValidText] = useState<string>();
const [outSegProblems, setOutSegProblems] = useState<{ error?: string, sameAsInputFileNameWarning?: boolean }>({ error: undefined, sameAsInputFileNameWarning: false });
const [outSegFileNames, setOutSegFileNames] = useState<string[]>();
const [shown, setShown] = useState<boolean>();
const inputRef = useRef<HTMLInputElement>(null);
const { t } = useTranslation();
@ -48,21 +51,25 @@ const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generate
setValidText(outSegs.outSegProblems.error == null ? debouncedText : undefined);
} catch (err) {
console.error(err);
setValidText();
setOutSegProblems({ error: err.message });
setValidText(undefined);
setOutSegProblems({ error: err instanceof Error ? err.message : String(err) });
}
}, [debouncedText, generateOutSegFileNames, t]);
// eslint-disable-next-line no-template-curly-in-string
const isMissingExtension = validText != null && !validText.endsWith(extVar);
const onAllSegmentsPreviewPress = () => ReactSwal.fire({
title: t('Resulting segment file names', { count: outSegFileNames.length }),
html: (
<div style={{ textAlign: 'left', overflowY: 'auto', maxHeight: 400 }}>
{outSegFileNames.map((f) => <div key={f} style={{ marginBottom: 7 }}>{f}</div>)}
</div>
) });
const onAllSegmentsPreviewPress = useCallback(() => {
if (outSegFileNames == null) return;
ReactSwal.fire({
title: t('Resulting segment file names', { count: outSegFileNames.length }),
html: (
<div style={{ textAlign: 'left', overflowY: 'auto', maxHeight: 400 }}>
{outSegFileNames.map((f) => <div key={f} style={{ marginBottom: 7 }}>{f}</div>)}
</div>
),
});
}, [outSegFileNames, t]);
useEffect(() => {
if (validText != null) setOutSegTemplate(validText);
@ -83,12 +90,14 @@ const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generate
const onTextChange = useCallback((e) => setText(e.target.value), []);
const needToShow = shown || outSegProblems.error != null || outSegProblems.sameAsInputFileNameWarning;
const gotImportantMessage = outSegProblems.error != null || outSegProblems.sameAsInputFileNameWarning;
const needToShow = shown || gotImportantMessage;
const onVariableClick = useCallback((variable) => {
const input = inputRef.current;
const startPos = input.selectionStart;
const endPos = input.selectionEnd;
const startPos = input!.selectionStart;
const endPos = input!.selectionEnd;
if (startPos == null || endPos == null) return;
const newValue = `${text.slice(0, startPos)}${`${formatVariable(variable)}${text.slice(endPos)}`}`;
setText(newValue);
@ -114,7 +123,7 @@ const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generate
{outSegFileNames != null && <Button height={20} onClick={onAllSegmentsPreviewPress} marginLeft={5}>{t('Preview')}</Button>}
<IconButton title={t('Reset')} icon={ResetIcon} height={20} onClick={reset} marginLeft={5} intent="danger" />
<IconButton title={t('Close')} icon={TickIcon} height={20} onClick={onHideClick} marginLeft={5} intent="success" appearance="primary" />
{!gotImportantMessage && <IconButton title={t('Close')} icon={TickIcon} height={20} onClick={onHideClick} marginLeft={5} intent="success" appearance="primary" />}
</div>
<div style={{ fontSize: '.8em', color: 'var(--gray11)', display: 'flex', gap: '.3em', flexWrap: 'wrap', alignItems: 'center', marginBottom: '.7em' }}>
@ -149,7 +158,7 @@ const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generate
{hasTextNumericPaddedValue && (
<div style={{ marginBottom: '.3em' }}>
<Select value={outputFileNameMinZeroPadding} onChange={(e) => setOutputFileNameMinZeroPadding(parseInt(e.target.value, 10))} style={{ marginRight: '1em', fontSize: '1em' }}>
{Array.from({ length: 10 }).map((v, i) => i + 1).map((v) => <option key={v} value={v}>{v}</option>)}
{Array.from({ length: 10 }).map((_v, i) => i + 1).map((v) => <option key={v} value={v}>{v}</option>)}
</Select>
Minimum numeric padded length
</div>

View File

@ -1,10 +0,0 @@
import { forwardRef } from 'react';
const inputStyle = { borderRadius: '.4em', flexGrow: 1, fontFamily: 'inherit', fontSize: '.8em', backgroundColor: 'var(--gray3)', color: 'var(--gray12)', border: '1px solid var(--gray7)', appearance: 'none' };
const TextInput = forwardRef(({ style, ...props }, forwardedRef) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<input type="text" ref={forwardedRef} style={{ ...inputStyle, ...style }} {...props} />
));
export default TextInput;

View File

@ -0,0 +1,10 @@
import { CSSProperties, forwardRef } from 'react';
const inputStyle: CSSProperties = { borderRadius: '.4em', flexGrow: 1, fontFamily: 'inherit', fontSize: '.8em', backgroundColor: 'var(--gray3)', color: 'var(--gray12)', border: '1px solid var(--gray7)', appearance: 'none' };
const TextInput = forwardRef<HTMLInputElement, JSX.IntrinsicElements['input']>(({ style, ...props }, forwardedRef) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<input type="text" ref={forwardedRef} style={{ ...inputStyle, ...style }} {...props} />
));
export default TextInput;

View File

@ -115,8 +115,9 @@ export async function askForEdlImport({ type, fps }: { type: EdlImportType, fps?
else if (type === 'llc') filters = [{ name: i18n.t('LosslessCut project'), extensions: ['llc'] }];
const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile'], filters });
if (canceled || filePaths.length === 0) return [];
return readEdlFile({ type, path: filePaths[0], fps });
const [firstFilePath] = filePaths;
if (canceled || firstFilePath == null) return [];
return readEdlFile({ type, path: firstFilePath, fps });
}
export async function exportEdlFile({ type, cutSegments, customOutDir, filePath, getFrameCount }: {

View File

@ -432,6 +432,7 @@ async function extractAttachmentStreams({ customOutDir, filePath, streams, enabl
const outPaths = await pMap(streams, async ({ index, codec_name: codec, codec_type: type }) => {
const ext = codec || 'bin';
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` });
if (outPath == null) throw new Error();
if (!enableOverwriteOutput && await pathExists(outPath)) throw new RefuseOverwriteError();
streamArgs = [

View File

@ -1,14 +1,14 @@
import { useEffect } from 'react';
import { RefObject, useEffect } from 'react';
import type { MenuItem, MenuItemConstructorOptions } from 'electron';
import useNativeMenu from './useNativeMenu';
// https://github.com/transflow/use-electron-context-menu
export default function useContextMenu(
ref,
template,
options = {},
ref: RefObject<HTMLElement>,
template: (MenuItemConstructorOptions | MenuItem)[],
) {
const { openMenu, closeMenu } = useNativeMenu(template, options);
const { openMenu, closeMenu } = useNativeMenu(template);
useEffect(() => {
const el = ref.current;

View File

@ -13,7 +13,7 @@ const { join, resolve, dirname } = window.require('path');
const { pathExists } = window.require('fs-extra');
const { writeFile, mkdir } = window.require('fs/promises');
async function writeChaptersFfmetadata(outDir, chapters) {
async function writeChaptersFfmetadata(outDir: string, chapters: { start: number, end: number, name?: string }[]) {
if (!chapters || chapters.length === 0) return undefined;
const path = join(outDir, `ffmetadata-${Date.now()}.txt`);
@ -26,8 +26,8 @@ async function writeChaptersFfmetadata(outDir, chapters) {
return path;
}
function getMovFlags({ preserveMovData, movFastStart }) {
const flags = [];
function getMovFlags({ preserveMovData, movFastStart }: { preserveMovData: boolean, movFastStart: boolean }) {
const flags: string[] = [];
// https://video.stackexchange.com/a/26084/29486
// https://github.com/mifi/lossless-cut/issues/331#issuecomment-623401794
@ -52,7 +52,7 @@ function getMatroskaFlags() {
const getChaptersInputArgs = (ffmetadataPath) => (ffmetadataPath ? ['-f', 'ffmetadata', '-i', ffmetadataPath] : []);
async function tryDeleteFiles(paths) {
async function tryDeleteFiles(paths: string[]) {
return pMap(paths, (path) => unlinkWithRetry(path).catch((err) => console.error('Failed to delete', path, err)), { concurrency: 5 });
}
@ -80,12 +80,12 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
}
try {
let inputArgs = [];
let inputArgs: string[] = [];
let inputIndex = 0;
// Keep track of input index to be used later
// eslint-disable-next-line no-inner-declarations
function addInput(args) {
function addInput(args: string[]) {
inputArgs = [...inputArgs, ...args];
const retIndex = inputIndex;
inputIndex += 1;
@ -245,10 +245,8 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
const mapStreamsArgs = getMapStreamsArgs({ copyFileStreams: copyFileStreamsFiltered, allFilesMeta, outFormat });
const customParamsArgs = (() => {
const ret = [];
// eslint-disable-next-line no-restricted-syntax
const ret: string[] = [];
for (const [fileId, fileParams] of paramsByStreamId.entries()) {
// eslint-disable-next-line no-restricted-syntax
for (const [streamId, streamParams] of fileParams.entries()) {
const outputIndex = mapInputStreamIndexToOutputIndex(fileId, parseInt(streamId, 10));
if (outputIndex != null) {
@ -269,7 +267,6 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
// custom stream metadata tags
const customTags = streamParams.get('customTags');
if (customTags != null) {
// eslint-disable-next-line no-restricted-syntax
for (const [tag, value] of Object.entries(customTags)) {
ret.push(`-metadata:s:${outputIndex}`, `${tag}=${value}`);
}
@ -285,7 +282,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
// No progress if we set loglevel warning :(
// '-loglevel', 'warning',
...getOutputPlaybackRateArgs(outputPlaybackRate),
...getOutputPlaybackRateArgs(),
...inputArgs,
@ -325,7 +322,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
logStdoutStderr(result);
await transferTimestamps({ inPath: filePath, outPath, cutFrom, cutTo, treatInputFileModifiedTimeAsStart, duration: isDurationValid(videoDuration) ? videoDuration : undefined, treatOutputFileModifiedTimeAsStart });
}, [cutFromAdjustmentFrames, filePath, getOutputPlaybackRateArgs, outputPlaybackRate, shouldSkipExistingFile, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]);
}, [cutFromAdjustmentFrames, filePath, getOutputPlaybackRateArgs, shouldSkipExistingFile, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]);
const cutMultiple = useCallback(async ({
outputDir, customOutDir, segments, outSegFileNames, videoDuration, rotation, detectedFps,
@ -388,6 +385,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
// eslint-disable-next-line no-shadow
async function cutEncodeSmartPartWrapper({ cutFrom, cutTo, outPath }) {
if (await shouldSkipExistingFile(outPath)) return;
if (videoCodec == null || videoBitrate == null || videoTimebase == null) throw new Error();
await cutEncodeSmartPart({ filePath, cutFrom, cutTo, outPath, outFormat, videoCodec, videoBitrate, videoStreamIndex, videoTimebase, allFilesMeta, copyFileStreams: copyFileStreamsFiltered, ffmpegExperimental });
}

View File

@ -1,6 +1,7 @@
import dataUriToBuffer from 'data-uri-to-buffer';
import pMap from 'p-map';
import { useCallback } from 'react';
import type * as FsPromises from 'fs/promises';
import { getSuffixedOutPath, getOutDir, transferTimestamps, getSuffixedFileName, getOutPath, escapeRegExp, fsOperationWithRetry } from '../util';
import { getNumDigits } from '../segments';
@ -8,7 +9,7 @@ import { getNumDigits } from '../segments';
import { captureFrame as ffmpegCaptureFrame, captureFrames as ffmpegCaptureFrames } from '../ffmpeg';
const mime = window.require('mime-types');
const { rename, readdir, writeFile } = window.require('fs/promises');
const { rename, readdir, writeFile }: typeof FsPromises = window.require('fs/promises');
function getFrameFromVideo(video, format, quality) {
@ -16,7 +17,7 @@ function getFrameFromVideo(video, format, quality) {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0);
canvas.getContext('2d')!.drawImage(video, 0, 0);
const dataUri = canvas.toDataURL(`image/${format}`, quality);
@ -24,8 +25,10 @@ function getFrameFromVideo(video, format, quality) {
}
export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => {
const captureFramesRange = useCallback(async ({ customOutDir, filePath, fps, fromTime, toTime, estimatedMaxNumFiles, captureFormat, quality, filter, onProgress, outputTimestamps }) => {
const getSuffix = (prefix) => `${prefix}.${captureFormat}`;
const captureFramesRange = useCallback(async ({ customOutDir, filePath, fps, fromTime, toTime, estimatedMaxNumFiles, captureFormat, quality, filter, onProgress, outputTimestamps }: {
customOutDir, filePath: string, fps: number, fromTime: number, toTime: number, estimatedMaxNumFiles: number, captureFormat: string, quality: number, filter?: string, onProgress: (a: number) => void, outputTimestamps: boolean
}) => {
const getSuffix = (prefix: string) => `${prefix}.${captureFormat}`;
if (!outputTimestamps) {
const numDigits = getNumDigits(estimatedMaxNumFiles);
@ -48,21 +51,21 @@ export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => {
const files = await readdir(outDir);
// https://github.com/mifi/lossless-cut/issues/1139
const matches = files.map((fileName) => {
const matches = files.flatMap((fileName) => {
const escapedRegexp = escapeRegExp(getSuffixedFileName(filePath, tmpSuffix));
const regexp = `^${escapedRegexp}(\\d+)`;
const match = fileName.match(new RegExp(regexp));
if (!match) return undefined;
const frameNum = parseInt(match[1], 10);
if (Number.isNaN(frameNum) || frameNum < 0) return undefined;
return { fileName, frameNum };
}).filter((it) => it != null);
if (!match) return [];
const frameNum = parseInt(match[1]!, 10);
if (Number.isNaN(frameNum) || frameNum < 0) return [];
return [{ fileName, frameNum }];
});
console.log('Renaming temp files...');
const outPaths = await pMap(matches, async ({ fileName, frameNum }) => {
const duration = formatTimecode({ seconds: fromTime + (frameNum / fps), fileNameFriendly: true });
const renameFromPath = getOutPath({ customOutDir, filePath, fileName });
const renameToPath = getOutPath({ customOutDir, filePath, fileName: getSuffixedFileName(filePath, getSuffix(duration, captureFormat)) });
const renameToPath = getOutPath({ customOutDir, filePath, fileName: getSuffixedFileName(filePath, getSuffix(duration)) });
await fsOperationWithRetry(async () => rename(renameFromPath, renameToPath));
return renameToPath;
}, { concurrency: 1 });
@ -70,7 +73,9 @@ export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => {
return outPaths[0];
}, [formatTimecode]);
const captureFrameFromFfmpeg = useCallback(async ({ customOutDir, filePath, fromTime, captureFormat, quality }) => {
const captureFrameFromFfmpeg = useCallback(async ({ customOutDir, filePath, fromTime, captureFormat, quality }: {
customOutDir?: string, filePath?: string, fromTime: number, captureFormat: string, quality: number,
}) => {
const time = formatTimecode({ seconds: fromTime, fileNameFriendly: true });
const nameSuffix = `${time}.${captureFormat}`;
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix });
@ -80,7 +85,9 @@ export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => {
return outPath;
}, [formatTimecode, treatOutputFileModifiedTimeAsStart]);
const captureFrameFromTag = useCallback(async ({ customOutDir, filePath, currentTime, captureFormat, video, quality }) => {
const captureFrameFromTag = useCallback(async ({ customOutDir, filePath, currentTime, captureFormat, video, quality }: {
customOutDir?: string, filePath?: string, currentTime: number, captureFormat: string, video: HTMLVideoElement, quality: number,
}) => {
const buf = getFrameFromVideo(video, captureFormat, quality);
const ext = mime.extension(buf.type);

View File

@ -1,20 +1,22 @@
import type { Menu as MenuType, MenuItemConstructorOptions, MenuItem } from 'electron';
import { useCallback, useMemo } from 'react';
// TODO pull out?
const remote = window.require('@electron/remote');
const { Menu } = remote;
// eslint-disable-next-line prefer-destructuring
const Menu: typeof MenuType = remote.Menu;
// https://github.com/transflow/use-electron-context-menu
// https://www.electronjs.org/docs/latest/api/menu-item
export default function useNativeMenu(
template,
options = {},
template: (MenuItemConstructorOptions | MenuItem)[],
options: { x?: number, y?: number, onContext?: (e: MouseEvent) => void, onClose?: () => void } = {},
) {
const menu = useMemo(() => Menu.buildFromTemplate(template), [template]);
const { x, y, onContext, onClose } = options;
const openMenu = useCallback((e) => {
const openMenu = useCallback((e: MouseEvent) => {
menu.popup({
window: remote.getCurrentWindow(),
x,

View File

@ -21,7 +21,7 @@ const { blackDetect, silenceDetect } = remote.require('./ffmpeg');
export default ({ filePath, workingRef, setWorking, setCutProgress, videoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly }: {
filePath: string, workingRef: MutableRefObject<boolean>, setWorking: (w: { text: string, abortController?: AbortController } | undefined) => void, setCutProgress: (a: number | undefined) => void, videoStream, duration?: number, getRelevantTime: () => number, maxLabelLength: number, checkFileOpened: () => boolean, invertCutSegments: boolean, segmentsToChaptersOnly: boolean,
filePath?: string, workingRef: MutableRefObject<boolean>, setWorking: (w: { text: string, abortController?: AbortController } | undefined) => void, setCutProgress: (a: number | undefined) => void, videoStream, duration?: number, getRelevantTime: () => number, maxLabelLength: number, checkFileOpened: () => boolean, invertCutSegments: boolean, segmentsToChaptersOnly: boolean,
}) => {
// Segment related state
const segCounterRef = useRef(0);
@ -266,6 +266,7 @@ export default ({ filePath, workingRef, setWorking, setCutProgress, videoStream,
async function align(key) {
const time = newSegment[key];
if (filePath == null) throw new Error();
const keyframe = await findKeyframeNearTime({ filePath, streamIndex: videoStream.index, time, mode });
if (keyframe == null) throw new Error(`Cannot find any keyframe within 60 seconds of frame ${time}`);
newSegment[key] = keyframe;

View File

@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client';
import { MotionConfig } from 'framer-motion';
import { enableMapSet } from 'immer';
import * as Electron from 'electron';
import Remote from '@electron/remote';
import 'sweetalert2/dist/sweetalert2.css';
@ -28,7 +29,10 @@ import './main.css';
declare global {
interface Window {
require: (module: 'electron') => typeof Electron;
require: <T extends string>(module: T) => T extends '@electron/remote' ? typeof Remote :
T extends 'electron' ? typeof Electron :
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any;
}
}

View File

@ -2,7 +2,7 @@ import { nanoid } from 'nanoid';
import sortBy from 'lodash/sortBy';
import minBy from 'lodash/minBy';
import maxBy from 'lodash/maxBy';
import { ApparentSegmentBase, InverseSegment, PlaybackMode, SegmentBase } from './types';
import { ApparentCutSegment, ApparentSegmentBase, InverseSegment, PlaybackMode, SegmentBase, SegmentTags } from './types';
export const isDurationValid = (duration?: number): duration is number => duration != null && Number.isFinite(duration) && duration > 0;
@ -50,7 +50,7 @@ export function findSegmentsAtCursor(apparentSegments, currentTime) {
return indexes;
}
export const getSegmentTags = (segment) => (segment.tags || {});
export const getSegmentTags = (segment: { tags?: SegmentTags | undefined }) => (segment.tags || {});
export const sortSegments = <T>(segments: T[]) => sortBy(segments, 'start');
@ -129,7 +129,7 @@ export function hasAnySegmentOverlap(sortedSegments) {
return overlappingGroups.length > 0;
}
export function invertSegments(sortedCutSegments, includeFirstSegment: boolean, includeLastSegment: boolean, duration?: number) {
export function invertSegments(sortedCutSegments: ApparentCutSegment[], includeFirstSegment: boolean, includeLastSegment: boolean, duration?: number) {
if (sortedCutSegments.length === 0) return undefined;
if (hasAnySegmentOverlap(sortedCutSegments)) return undefined;
@ -137,7 +137,7 @@ export function invertSegments(sortedCutSegments, includeFirstSegment: boolean,
const ret: InverseSegment[] = [];
if (includeFirstSegment) {
const firstSeg = sortedCutSegments[0];
const firstSeg = sortedCutSegments[0]!;
if (firstSeg.start > 0) {
ret.push({
start: 0,
@ -149,7 +149,7 @@ export function invertSegments(sortedCutSegments, includeFirstSegment: boolean,
sortedCutSegments.forEach((cutSegment, i) => {
if (i === 0) return;
const previousSeg = sortedCutSegments[i - 1];
const previousSeg = sortedCutSegments[i - 1]!;
const inverted: InverseSegment = {
start: previousSeg.end,
end: cutSegment.start,

View File

@ -9,21 +9,34 @@ export interface ApparentSegmentBase {
}
export type SegmentTags = Record<string, string>;
export interface StateSegment extends SegmentBase {
name: string;
segId: string;
segColorIndex?: number | undefined;
tags?: Record<string, string> | undefined;
tags?: SegmentTags | undefined;
}
export interface Segment extends SegmentBase {
name?: string,
}
export interface InverseSegment extends SegmentBase {
export interface ApparentCutSegment extends ApparentSegmentBase {
segId?: string | undefined,
tags?: SegmentTags | undefined;
}
export interface InverseSegment extends ApparentSegmentBase {
segId?: string,
}
export interface SegmentToExport extends ApparentSegmentBase {
name?: string | undefined;
segId?: string | undefined;
tags?: SegmentTags | undefined;
}
export type PlaybackMode = 'loop-segment-start-end' | 'loop-segment' | 'play-segment-once' | 'loop-selected-segments';
export type Html5ifyMode = 'fastest' | 'fast-audio-remux' | 'fast-audio' | 'fast' | 'slow' | 'slow-audio' | 'slowest';

View File

@ -3,17 +3,21 @@ import pMap from 'p-map';
import ky from 'ky';
import prettyBytes from 'pretty-bytes';
import sortBy from 'lodash/sortBy';
import pRetry from 'p-retry';
import pRetry, { Options } from 'p-retry';
import { ExecaError } from 'execa';
import type * as FsPromises from 'fs/promises';
import type * as Os from 'os';
import type * as FsExtra from 'fs-extra';
import type { PlatformPath } from 'path';
import isDev from './isDev';
import Swal, { toast } from './swal';
import { ffmpegExtractWindow } from './util/constants';
const { dirname, parse: parsePath, join, extname, isAbsolute, resolve, basename } = window.require('path');
const fsExtra = window.require('fs-extra');
const { stat, lstat, readdir, utimes, unlink } = window.require('fs/promises');
const os = window.require('os');
const { dirname, parse: parsePath, join, extname, isAbsolute, resolve, basename }: PlatformPath = window.require('path');
const fsExtra: typeof FsExtra = window.require('fs-extra');
const { stat, lstat, readdir, utimes, unlink }: typeof FsPromises = window.require('fs/promises');
const os: typeof Os = window.require('os');
const { ipcRenderer } = window.require('electron');
const remote = window.require('@electron/remote');
@ -27,9 +31,10 @@ export function getFileDir(filePath?: string) {
return filePath ? dirname(filePath) : undefined;
}
export function getOutDir(customOutDir?: string, filePath?: string) {
if (customOutDir) return customOutDir;
if (filePath) return getFileDir(filePath);
export function getOutDir<T1 extends string | undefined, T2 extends string | undefined>(customOutDir?: T1, filePath?: T2): T1 extends string ? string : T2 extends string ? string : undefined;
export function getOutDir(customOutDir?: string | undefined, filePath?: string | undefined) {
if (customOutDir != null) return customOutDir;
if (filePath != null) return getFileDir(filePath);
return undefined;
}
@ -39,15 +44,17 @@ function getFileBaseName(filePath?: string) {
return parsed.name;
}
export function getOutPath({ customOutDir, filePath, fileName }: { customOutDir?: string, filePath?: string, fileName?: string }) {
if (!filePath) return undefined;
export function getOutPath<T extends string | undefined>(a: { customOutDir?: string, filePath?: T, fileName: string }): T extends string ? string : undefined;
export function getOutPath({ customOutDir, filePath, fileName }: { customOutDir?: string, filePath?: string | undefined, fileName: string }) {
if (filePath == null) return undefined;
return join(getOutDir(customOutDir, filePath), fileName);
}
export const getSuffixedFileName = (filePath: string | undefined, nameSuffix: string) => `${getFileBaseName(filePath)}-${nameSuffix}`;
export function getSuffixedOutPath({ customOutDir, filePath, nameSuffix }: { customOutDir?: string, filePath?: string, nameSuffix: string }) {
if (!filePath) return undefined;
export function getSuffixedOutPath<T extends string | undefined>(a: { customOutDir?: string, filePath?: T, nameSuffix: string }): T extends string ? string : undefined;
export function getSuffixedOutPath({ customOutDir, filePath, nameSuffix }: { customOutDir?: string, filePath?: string | undefined, nameSuffix: string }) {
if (filePath == null) return undefined;
return getOutPath({ customOutDir, filePath, fileName: getSuffixedFileName(filePath, nameSuffix) });
}
@ -100,7 +107,7 @@ export async function dirExists(dirPath) {
const testFailFsOperation = false;
// Retry because sometimes write operations fail on windows due to the file being locked for various reasons (often anti-virus) #272 #1797 #1704
export async function fsOperationWithRetry(operation, { signal, retries = 10, minTimeout = 100, maxTimeout = 2000, ...opts }) {
export async function fsOperationWithRetry(operation, { signal, retries = 10, minTimeout = 100, maxTimeout = 2000, ...opts }: Options & { retries?: number, minTimeout?: number, maxTimeout?: number } = {}) {
return pRetry(async () => {
if (testFailFsOperation && Math.random() > 0.3) throw Object.assign(new Error('test delete failure'), { code: 'EPERM' });
await operation();
@ -116,10 +123,9 @@ export async function fsOperationWithRetry(operation, { signal, retries = 10, mi
}
// example error: index-18074aaf.js:166 Failed to delete C:\Users\USERNAME\Desktop\RC\New folder\2023-12-27 21-45-22 (GMT p5)-merged-1703933052361-00.01.04.915-00.01.07.424-seg1.mp4 Error: EPERM: operation not permitted, unlink 'C:\Users\USERNAME\Desktop\RC\New folder\2023-12-27 21-45-22 (GMT p5)-merged-1703933052361-00.01.04.915-00.01.07.424-seg1.mp4'
export const unlinkWithRetry = async (path, options) => fsOperationWithRetry(async () => unlink(path), { ...options, onFailedAttempt: (error) => console.warn('Retrying delete', path, error.attemptNumber) });
export const unlinkWithRetry = async (path: string, options?: Options) => fsOperationWithRetry(async () => unlink(path), { ...options, onFailedAttempt: (error) => console.warn('Retrying delete', path, error.attemptNumber) });
// example error: index-18074aaf.js:160 Error: EPERM: operation not permitted, utime 'C:\Users\USERNAME\Desktop\RC\New folder\2023-12-27 21-45-22 (GMT p5)-merged-1703933052361-cut-merged-1703933070237.mp4'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const utimesWithRetry = async (path: string, atime: number, mtime: number, options?: any) => fsOperationWithRetry(async () => utimes(path, atime, mtime), { ...options, onFailedAttempt: (error) => console.warn('Retrying utimes', path, error.attemptNumber) });
export const utimesWithRetry = async (path: string, atime: number, mtime: number, options?: Options) => fsOperationWithRetry(async () => utimes(path, atime, mtime), { ...options, onFailedAttempt: (error) => console.warn('Retrying utimes', path, error.attemptNumber) });
export const getFrameDuration = (fps?: number) => 1 / (fps ?? 30);
@ -192,7 +198,7 @@ export const arch = os.arch();
export const isWindows = platform === 'win32';
export const isMac = platform === 'darwin';
export function getExtensionForFormat(format) {
export function getExtensionForFormat(format: string) {
const ext = {
matroska: 'mkv',
ipod: 'm4a',
@ -203,7 +209,9 @@ export function getExtensionForFormat(format) {
return ext || format;
}
export function getOutFileExtension({ isCustomFormatSelected, outFormat, filePath }) {
export function getOutFileExtension({ isCustomFormatSelected, outFormat, filePath }: {
isCustomFormatSelected?: boolean, outFormat: string, filePath: string,
}) {
if (!isCustomFormatSelected) {
const ext = extname(filePath);
// QuickTime is quirky about the file extension of mov files (has to be .mov)
@ -232,11 +240,12 @@ export async function findExistingHtml5FriendlyFile(fp, cod) {
const prefix = getSuffixedFileName(fp, html5ifiedPrefix);
const outDir = getOutDir(cod, fp);
if (outDir == null) throw new Error();
const dirEntries = await readdir(outDir);
const html5ifiedDirEntries = dirEntries.filter((entry) => entry.startsWith(prefix));
let matches: { entry: string, suffix: string }[] = [];
let matches: { entry: string, suffix?: string }[] = [];
suffixes.forEach((suffix) => {
const entryWithSuffix = html5ifiedDirEntries.find((entry) => new RegExp(`${suffix}\\..*$`).test(entry.replace(prefix, '')));
if (entryWithSuffix) matches = [...matches, { entry: entryWithSuffix, suffix }];
@ -325,6 +334,7 @@ export async function checkAppPath() {
return;
}
const pathSeg = pathMatch[1];
if (pathSeg == null) return;
if (pathSeg.startsWith(`57275${mf}.${llc}_`)) return;
// this will report the path and may return a msg
const url = `https://losslesscut-analytics.mifi.no/${pathSeg.length}/${encodeURIComponent(btoa(pathSeg))}`;
@ -381,7 +391,7 @@ function setDocumentExtraTitle(extra) {
document.title = extra != null ? `${baseTitle} - ${extra}` : baseTitle;
}
export function setDocumentTitle({ filePath, working, cutProgress }: { filePath: string, working?: string, cutProgress?: number }) {
export function setDocumentTitle({ filePath, working, cutProgress }: { filePath?: string, working?: string, cutProgress?: number }) {
const parts: string[] = [];
if (filePath) parts.push(basename(filePath));
if (working) {

View File

@ -1,22 +1,25 @@
import i18n from 'i18next';
import lodashTemplate from 'lodash/template';
import { PlatformPath } from 'path';
import { isMac, isWindows, hasDuplicates, filenamify, getOutFileExtension } from '../util';
import isDev from '../isDev';
import { getSegmentTags, formatSegNum } from '../segments';
import { Segment } from '../types';
import { SegmentToExport } from '../types';
export const segNumVariable = 'SEG_NUM';
export const segSuffixVariable = 'SEG_SUFFIX';
const { parse: parsePath, sep: pathSep, join: pathJoin, normalize: pathNormalize, basename } = window.require('path');
const { parse: parsePath, sep: pathSep, join: pathJoin, normalize: pathNormalize, basename }: PlatformPath = window.require('path');
function getOutSegProblems({ fileNames, filePath, outputDir, safeOutputFileName }) {
let error;
function getOutSegProblems({ fileNames, filePath, outputDir, safeOutputFileName }: {
fileNames: string[], filePath: string, outputDir: string, safeOutputFileName: boolean
}) {
let error: string | undefined;
let sameAsInputFileNameWarning = false;
// eslint-disable-next-line no-restricted-syntax
for (const fileName of fileNames) {
if (!filePath) {
error = i18n.t('No file is loaded');
@ -115,7 +118,7 @@ function interpolateSegmentFileName({ template, epochMs, inputFileNameWithoutExt
}
export function generateOutSegFileNames({ segments, template: desiredTemplate, formatTimecode, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding }: {
segments: Segment[], template: string, formatTimecode: (a: { seconds?: number, shorten?: boolean, fileNameFriendly?: boolean }) => string, isCustomFormatSelected: boolean, fileFormat?: string, filePath: string, outputDir: string, safeOutputFileName: string, maxLabelLength: number, outputFileNameMinZeroPadding: number,
segments: SegmentToExport[], template: string, formatTimecode: (a: { seconds?: number, shorten?: boolean, fileNameFriendly?: boolean }) => string, isCustomFormatSelected: boolean, fileFormat: string, filePath: string, outputDir: string, safeOutputFileName: boolean, maxLabelLength: number, outputFileNameMinZeroPadding: number,
}) {
function generate({ template, forceSafeOutputFileName }) {
const epochMs = Date.now();
@ -126,7 +129,7 @@ export function generateOutSegFileNames({ segments, template: desiredTemplate, f
// Fields that did not come from the source file's name must be sanitized, because they may contain characters that are not supported by the target operating/file system
// however we disable this when the user has chosen to (safeOutputFileName === false)
const filenamifyOrNot = (fileName) => (safeOutputFileName || forceSafeOutputFileName ? filenamify(fileName) : fileName).slice(0, Math.max(0, maxLabelLength));
const filenamifyOrNot = (fileName: string) => (safeOutputFileName || forceSafeOutputFileName ? filenamify(fileName) : fileName).slice(0, Math.max(0, maxLabelLength));
function getSegSuffix() {
if (name) return `-${filenamifyOrNot(name)}`;

View File

@ -106,7 +106,7 @@ export function getActiveDisposition(disposition) {
return existingActiveDispositionEntry[0]; // return the key
}
export const isMov = (format) => ['ismv', 'ipod', 'mp4', 'mov'].includes(format);
export const isMov = (format: string) => ['ismv', 'ipod', 'mp4', 'mov'].includes(format);
type GetVideoArgsFn = (a: { streamIndex: number, outputIndex: number }) => string[] | undefined;

View File

@ -1809,6 +1809,15 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:18":
version: 18.19.21
resolution: "@types/node@npm:18.19.21"
dependencies:
undici-types: "npm:~5.26.4"
checksum: 3a5c5841f294bc35b5b416a32764b5c0c2f22f4cef48cb7d2e3b4e068a52d5857e50da8e6e0685e743127c70344301c833849a3904ce3bd3f67448da5e85487a
languageName: node
linkType: hard
"@types/node@npm:^18.11.18":
version: 18.17.6
resolution: "@types/node@npm:18.17.6"
@ -7699,6 +7708,7 @@ __metadata:
"@tsconfig/vite-react": "npm:^3.0.0"
"@types/eslint": "npm:^8"
"@types/lodash": "npm:^4.14.202"
"@types/node": "npm:18"
"@types/sortablejs": "npm:^1.15.0"
"@typescript-eslint/eslint-plugin": "npm:^6.12.0"
"@typescript-eslint/parser": "npm:^6.12.0"
@ -11748,6 +11758,13 @@ __metadata:
languageName: node
linkType: hard
"undici-types@npm:~5.26.4":
version: 5.26.5
resolution: "undici-types@npm:5.26.5"
checksum: 0097779d94bc0fd26f0418b3a05472410408877279141ded2bd449167be1aed7ea5b76f756562cb3586a07f251b90799bab22d9019ceba49c037c76445f7cddd
languageName: node
linkType: hard
"unique-filename@npm:^2.0.0":
version: 2.0.1
resolution: "unique-filename@npm:2.0.1"