@@ -118,12 +124,17 @@ const ExportConfirm = memo(({
- Input has {{ numStreamsTotal }} tracks - setStreamsSelectorShown(true)}>Keeping {{ numStreamsToCopy }} tracks
+ Input has {{ numStreamsTotal }} tracks - setStreamsSelectorShown(true)}>Keeping {{ numStreamsToCopy }} tracks
{t('Save output to path:')} {outputDir}
+ {(outSegments.length === 1 || !autoMerge) && (
+
+
+
+ )}
{t('Advanced options')}
diff --git a/src/components/HighlightedText.jsx b/src/components/HighlightedText.jsx
new file mode 100644
index 00000000..7018a8e2
--- /dev/null
+++ b/src/components/HighlightedText.jsx
@@ -0,0 +1,6 @@
+import React, { memo } from 'react';
+
+// eslint-disable-next-line react/jsx-props-no-spreading
+const HighlightedText = memo(({ children, style, ...props }) => {children});
+
+export default HighlightedText;
diff --git a/src/components/OutSegTemplateEditor.jsx b/src/components/OutSegTemplateEditor.jsx
new file mode 100644
index 00000000..ffdae3e7
--- /dev/null
+++ b/src/components/OutSegTemplateEditor.jsx
@@ -0,0 +1,90 @@
+import React, { memo, useState, useEffect } from 'react';
+import { useDebounce } from 'use-debounce';
+import { useTranslation } from 'react-i18next';
+import { Button, Alert } from 'evergreen-ui';
+import Swal from 'sweetalert2';
+import withReactContent from 'sweetalert2-react-content';
+
+import HighlightedText from './HighlightedText';
+import { defaultOutSegTemplate } from '../util';
+
+const ReactSwal = withReactContent(Swal);
+
+
+const OutSegTemplateEditor = memo(({ helpIcon, outSegTemplate, setOutSegTemplate, generateOutSegFileNames, currentSegIndexSafe, isOutSegFileNamesValid }) => {
+ const [text, setText] = useState(outSegTemplate);
+ const [debouncedText] = useDebounce(text, 500);
+ const [validText, setValidText] = useState();
+ const [error, setError] = useState();
+ const [outSegFileNames, setOutSegFileNames] = useState();
+ const [shown, setShown] = useState();
+
+ const { t } = useTranslation();
+
+ useEffect(() => {
+ if (debouncedText == null) return;
+
+ try {
+ const generatedOutSegFileNames = generateOutSegFileNames({ template: debouncedText });
+ setOutSegFileNames(generatedOutSegFileNames);
+ const isOutSegTemplateValid = isOutSegFileNamesValid(generatedOutSegFileNames);
+ if (!isOutSegTemplateValid) {
+ setError(t('This template will result in invalid file names'));
+ setValidText();
+ return;
+ }
+
+ setValidText(debouncedText);
+ setError();
+ } catch (err) {
+ console.error(err);
+ setValidText();
+ setError(err.message);
+ }
+ }, [debouncedText, generateOutSegFileNames, isOutSegFileNamesValid, t]);
+
+ const onAllSegmentsPreviewPress = () => ReactSwal.fire({ title: t('Resulting segment file names'), html: {outSegFileNames.map((f) =>
{f}
)}
});
+
+ useEffect(() => {
+ if (validText != null) setOutSegTemplate(validText);
+ }, [validText, setOutSegTemplate]);
+
+ function reset() {
+ setOutSegTemplate(defaultOutSegTemplate);
+ setText(defaultOutSegTemplate);
+ }
+
+ function onToggleClick() {
+ if (!shown) setShown(true);
+ else if (error == null) setShown(false);
+ }
+
+ return (
+ <>
+
+
+ {t('Output name(s):')} {outSegFileNames != null && {outSegFileNames[currentSegIndexSafe]}}
+
+ {helpIcon}
+
+
+ {shown && (
+ <>
+
+ setText(e.target.value)} value={text} autoComplete="off" autoCapitalize="off" autoCorrect="off" />
+ {outSegFileNames && }
+
+
+
+
+ {error != null &&
{`There is an error in the file name template: ${error}`}}
+ {/* eslint-disable-next-line no-template-curly-in-string */}
+
{'Variables: ${FILENAME} ${CUT_FROM} ${CUT_TO} ${SEG_NUM} ${SEG_LABEL} ${SEG_SUFFIX} ${EXT}'}
+
+ >
+ )}
+ >
+ );
+});
+
+export default OutSegTemplateEditor;
diff --git a/src/ffmpeg.js b/src/ffmpeg.js
index 9bb64710..160d55dc 100644
--- a/src/ffmpeg.js
+++ b/src/ffmpeg.js
@@ -7,10 +7,10 @@ import moment from 'moment';
import i18n from 'i18next';
import Timecode from 'smpte-timecode';
-import { formatDuration, getOutPath, getOutDir, transferTimestamps, filenamify, isDurationValid } from './util';
+import { getOutPath, getOutDir, transferTimestamps, isDurationValid, getExtensionForFormat, getOutFileExtension } from './util';
const execa = window.require('execa');
-const { join, extname } = window.require('path');
+const { join } = window.require('path');
const fileType = window.require('file-type');
const readChunk = window.require('read-chunk');
const readline = window.require('readline');
@@ -88,15 +88,6 @@ export function isCuttingEnd(cutTo, duration) {
return cutTo < duration;
}
-function getExtensionForFormat(format) {
- const ext = {
- matroska: 'mkv',
- ipod: 'm4a',
- }[format];
-
- return ext || format;
-}
-
function getIntervalAroundTime(time, window) {
return {
from: Math.max(time - window / 2, 0),
@@ -296,15 +287,11 @@ async function cut({
await transferTimestamps(filePath, outPath);
}
-function getOutFileExtension({ isCustomFormatSelected, outFormat, filePath }) {
- return isCustomFormatSelected ? `.${getExtensionForFormat(outFormat)}` : extname(filePath);
-}
-
export async function cutMultiple({
- customOutDir, filePath, segments, videoDuration, rotation,
- onProgress, keyframeCut, copyFileStreams, outFormat, isCustomFormatSelected,
+ outputDir, filePath, segments, segmentsFileNames, videoDuration, rotation,
+ onProgress, keyframeCut, copyFileStreams, outFormat,
appendFfmpegCommandLog, shortestFlag, ffmpegExperimental, preserveMovData, movFastStart, avoidNegativeTs,
- customTagsByFile, customTagsByStreamId, invertCutSegments,
+ customTagsByFile, customTagsByStreamId,
}) {
console.log('customTagsByFile', customTagsByFile);
console.log('customTagsByStreamId', customTagsByStreamId);
@@ -317,21 +304,11 @@ export async function cutMultiple({
const outFiles = [];
- let i = 0;
// eslint-disable-next-line no-restricted-syntax,no-unused-vars
- for (const { start, end, name, order } of segments) {
- const cutFromStr = formatDuration({ seconds: start, fileNameFriendly: true });
- const cutToStr = formatDuration({ seconds: end, fileNameFriendly: true });
- let segNamePart = '';
- if (!invertCutSegments) {
- if (name) segNamePart = `-${filenamify(name)}`;
- // https://github.com/mifi/lossless-cut/issues/583
- else if (segments.length > 1) segNamePart = `-seg${order + 1}`;
- }
- const cutSpecification = `${cutFromStr}-${cutToStr}${segNamePart}`.substr(0, 200);
- const ext = getOutFileExtension({ isCustomFormatSelected, outFormat, filePath });
- const fileName = `${cutSpecification}${ext}`;
- const outPath = getOutPath(customOutDir, filePath, fileName);
+ for (const [i, { start, end }] of segments.entries()) {
+ const fileName = segmentsFileNames[i];
+
+ const outPath = join(outputDir, fileName);
// eslint-disable-next-line no-await-in-loop
await cut({
@@ -357,8 +334,6 @@ export async function cutMultiple({
});
outFiles.push(outPath);
-
- i += 1;
}
return outFiles;
diff --git a/src/util.js b/src/util.js
index ecc4e934..008b2cce 100644
--- a/src/util.js
+++ b/src/util.js
@@ -1,6 +1,7 @@
import padStart from 'lodash/padStart';
import Swal from 'sweetalert2';
import i18n from 'i18next';
+import lodashTemplate from 'lodash/template';
import randomColor from './random-color';
@@ -166,3 +167,26 @@ export const isDurationValid = (duration) => Number.isFinite(duration) && durati
const platform = os.platform();
export const isWindows = platform === 'win32';
+
+export function getExtensionForFormat(format) {
+ const ext = {
+ matroska: 'mkv',
+ ipod: 'm4a',
+ }[format];
+
+ return ext || format;
+}
+
+export function getOutFileExtension({ isCustomFormatSelected, outFormat, filePath }) {
+ return isCustomFormatSelected ? `.${getExtensionForFormat(outFormat)}` : path.extname(filePath);
+}
+
+// eslint-disable-next-line no-template-curly-in-string
+export const defaultOutSegTemplate = '${FILENAME}-${CUT_FROM}-${CUT_TO}${SEG_SUFFIX}${EXT}';
+
+export function generateSegFileName({ template, inputFileNameWithoutExt, segSuffix, ext, segNum, segLabel, cutFrom, cutTo }) {
+ const compiled = lodashTemplate(template);
+ return compiled({ FILENAME: inputFileNameWithoutExt, SEG_SUFFIX: segSuffix, EXT: ext, SEG_NUM: segNum, SEG_LABEL: segLabel, CUT_FROM: cutFrom, CUT_TO: cutTo });
+}
+
+export const hasDuplicates = (arr) => new Set(arr).size !== arr.length;