You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lossless-cut/src/util/outputNameTemplate.js

139 lines
5.5 KiB
JavaScript

import i18n from 'i18next';
import lodashTemplate from 'lodash/template';
import { isMac, isWindows, hasDuplicates, filenamify, getOutFileExtension } from '../util';
import isDev from '../isDev';
import { getSegmentTags, formatSegNum } from '../segments';
export const segNumVariable = 'SEG_NUM';
export const segSuffixVariable = 'SEG_SUFFIX';
const { parse: parsePath, sep: pathSep, join: pathJoin, normalize: pathNormalize } = window.require('path');
// eslint-disable-next-line import/prefer-default-export
export function getOutSegError({ fileNames, filePath, outputDir, safeOutputFileName }) {
let error;
// eslint-disable-next-line no-restricted-syntax
for (const fileName of fileNames) {
if (!filePath) {
error = 'No file path';
break;
}
const invalidChars = new Set();
// https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
// note that we allow path separators!
if (isWindows) {
['<', '>', ':', '"', '|', '?', '*'].forEach((char) => invalidChars.add(char));
} else if (isMac) {
// Colon is invalid on windows https://github.com/mifi/lossless-cut/issues/631 and on MacOS, but not Linux https://github.com/mifi/lossless-cut/issues/830
[':'].forEach((char) => invalidChars.add(char));
}
if (safeOutputFileName) invalidChars.add(pathSep);
const outPath = pathNormalize(pathJoin(outputDir, fileName));
const sameAsInputPath = outPath === pathNormalize(filePath);
const windowsMaxPathLength = 259;
const shouldCheckPathLength = isWindows || isDev;
if (fileName.length === 0) {
error = i18n.t('At least one resulting file name has no length');
break;
}
if ([...fileName].some((char) => invalidChars.has(char))) {
error = i18n.t('At least one resulting file name contains invalid characters');
break;
}
if (sameAsInputPath) {
error = i18n.t('At least one resulting file name is the same as the input path');
break;
}
if (shouldCheckPathLength && outPath.length >= windowsMaxPathLength) {
error = i18n.t('At least one resulting file will have a too long path');
break;
}
}
if (error != null) return error;
if (hasDuplicates(fileNames)) return i18n.t('Output file name template results in duplicate file names (you are trying to export multiple files with the same name). You can fix this for example by adding the "{{segNumVariable}}" variable.', { segNumVariable });
return undefined;
}
// This is used as a fallback and so it has to always generate unique file names
// eslint-disable-next-line no-template-curly-in-string
export const defaultOutSegTemplate = '${FILENAME}-${CUT_FROM}-${CUT_TO}${SEG_SUFFIX}${EXT}';
function interpolateSegmentFileName({ template, epochMs, inputFileNameWithoutExt, segSuffix, ext, segNum, segLabel, cutFrom, cutTo, tags }) {
const compiled = lodashTemplate(template);
const data = {
FILENAME: inputFileNameWithoutExt,
SEG_SUFFIX: segSuffix,
EXT: ext,
SEG_NUM: segNum,
SEG_LABEL: segLabel,
EPOCH_MS: String(epochMs),
CUT_FROM: cutFrom,
CUT_TO: cutTo,
SEG_TAGS: {
// allow both original case and uppercase
...tags,
...Object.fromEntries(Object.entries(tags).map(([key, value]) => [`${key.toLocaleUpperCase('en-US')}`, value])),
},
};
return compiled(data);
}
export function generateOutSegFileNames({ segments, template, forceSafeOutputFileName, formatTimecode, isCustomFormatSelected, fileFormat, filePath, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding }) {
const epochMs = Date.now();
return segments.map((segment, i) => {
const { start, end, name = '' } = segment;
const segNum = formatSegNum(i, segments.length, outputFileNameMinZeroPadding);
// 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).substr(0, maxLabelLength);
function getSegSuffix() {
if (name) return `-${filenamifyOrNot(name)}`;
// https://github.com/mifi/lossless-cut/issues/583
if (segments.length > 1) return `-seg${segNum}`;
return '';
}
const { name: inputFileNameWithoutExt } = parsePath(filePath);
const segFileName = interpolateSegmentFileName({
template,
epochMs,
segNum,
inputFileNameWithoutExt,
segSuffix: getSegSuffix(),
ext: getOutFileExtension({ isCustomFormatSelected, outFormat: fileFormat, filePath }),
segLabel: filenamifyOrNot(name),
cutFrom: formatTimecode({ seconds: start, fileNameFriendly: true }),
cutTo: formatTimecode({ seconds: end, fileNameFriendly: true }),
tags: Object.fromEntries(Object.entries(getSegmentTags(segment)).map(([tag, value]) => [tag, filenamifyOrNot(value)])),
});
// Now split the path by its separator, so we can check the actual file name (last path seg)
const pathSegs = segFileName.split(pathSep);
if (pathSegs.length === 0) return '';
const [lastSeg] = pathSegs.slice(-1);
const rest = pathSegs.slice(0, -1);
return [
...rest,
// If sanitation is enabled, make sure filename (last seg of the path) is not too long
safeOutputFileName ? lastSeg.substring(0, 200) : lastSeg,
].join(pathSep);
});
}