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

upgrade lint

This commit is contained in:
Mikael Finstad 2024-02-20 22:48:40 +08:00
parent e7c85dd8b5
commit 4704d246b4
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
48 changed files with 1745 additions and 767 deletions

View File

@ -1,47 +1,37 @@
module.exports = {
extends: [
'airbnb',
'airbnb/hooks',
'eslint:recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/stylistic',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
],
extends: ['mifi'],
rules: {
'no-console': 0,
'jsx-a11y/click-events-have-key-events': 0,
'jsx-a11y/interactive-supports-focus': 0,
'jsx-a11y/control-has-associated-label': 0,
'unicorn/prefer-node-protocol': 0, // todo
'@typescript-eslint/no-var-requires': 0, // todo
'react/display-name': 0, // todo
},
overrides: [
{
files: ['./src/**/*.{js,cjs,mjs,jsx,ts,tsx,mts}'],
env: {
node: true,
node: false,
browser: true,
},
rules: {
'max-len': 0,
'import/no-extraneous-dependencies': ['error', {
devDependencies: true,
optionalDependencies: false,
}],
'no-console': 0,
'jsx-a11y/click-events-have-key-events': 0,
'jsx-a11y/interactive-supports-focus': 0,
'react/jsx-one-expression-per-line': 0,
'object-curly-newline': 0,
'arrow-parens': 0,
'jsx-a11y/control-has-associated-label': 0,
'react/prop-types': 0,
'no-multiple-empty-lines': ['error', { max: 2, maxBOF: 0, maxEOF: 0 }],
'no-promise-executor-return': 0,
'react/function-component-definition': 0,
'no-constant-binary-expression': 'error',
'@typescript-eslint/no-var-requires': 0, // todo
'react/display-name': 0, // todo
'@typescript-eslint/no-unused-vars': 0, // todo
'import/extensions': 0, // doesn't work with TS https://github.com/import-js/eslint-plugin-import/issues/2111
'react/jsx-filename-extension': ['error', { extensions: ['.jsx', '.tsx'] }],
'react/require-default-props': 0,
},
parserOptions: {
ecmaVersion: 2022,
},
{
files: ['./script/**/*.{js,cjs,mjs,jsx,ts,tsx,mts}', 'vite.config.js'],
rules: {
'import/no-extraneous-dependencies': ['error', {
devDependencies: true,
optionalDependencies: false,
}],
},
},
],
};

View File

@ -21,10 +21,10 @@ Note: `yarn` may take some time to complete.
Run one of the below commands:
```bash
npm run download-ffmpeg-darwin-x64
npm run download-ffmpeg-darwin-arm64
npm run download-ffmpeg-linux-x64
npm run download-ffmpeg-win32-x64
yarn download-ffmpeg-darwin-x64
yarn download-ffmpeg-darwin-arm64
yarn download-ffmpeg-linux-x64
yarn download-ffmpeg-win32-x64
```
For Windows, you may have to install [7z](https://www.7-zip.org/download.html), and then put the 7z folder in your `PATH`.
@ -32,7 +32,7 @@ For Windows, you may have to install [7z](https://www.7-zip.org/download.html),
### Running
```bash
npm start
yarn dev
```
## `mas-dev` (Mac App Store) local build
@ -40,7 +40,7 @@ npm start
This will sign using the development provisioning profile:
```
npm run pack-mas-dev
yarn pack-mas-dev
```
MAS builds have some restrictions, see `isMasBuild` variable in code. In particular, any file cannot be read without the user's consent.
@ -86,7 +86,7 @@ For per-platform build/signing setup, see [this article](https://mifi.no/blog/au
- If Mac App Store / Windows Store
- Merge `stores` into `master`
- Bump [snap version](https://snapcraft.io/losslesscut/listing)
- `npm run scan-i18n` to get the newest English strings and push so weblate gets them
- `yarn scan-i18n` to get the newest English strings and push so weblate gets them
## Minimum OS version
@ -100,7 +100,7 @@ Minimum supported OS versions for Electron. As of electron 22:
How to check the value:
```bash
npm run pack-mas-dev
yarn pack-mas-dev
cat dist/mas-dev-arm64/LosslessCut.app/Contents/Info.plist
```
@ -126,7 +126,7 @@ Links:
- package.json
### i18n
`npm run scan-i18n`
`yarn scan-i18n`
### Licenses
@ -139,7 +139,7 @@ npx license-checker --summary
#### Regenerate licenses file
```
npm run generate-licenses
yarn generate-licenses
#cp licenses.txt losslesscut.mifi.no/public/
```
Then deploy.

View File

@ -1,3 +1,4 @@
// eslint-disable-line unicorn/filename-case
export default {
input: ['src/**/*.{js,jsx,ts,tsx}', 'public/*.{js,ts}'],

View File

@ -7,9 +7,9 @@
"main": "public/electron.js",
"homepage": "./",
"scripts": {
"start": "concurrently -k \"npm run start:frontend\" \"npm run start:electron\"",
"start:frontend": "cross-env vite --port 3001",
"start:electron": "wait-on tcp:3001 && electron .",
"dev": "concurrently -k \"npm run dev:frontend\" \"npm run dev:electron\"",
"dev:frontend": "cross-env vite --port 3001",
"dev:electron": "wait-on tcp:3001 && electron .",
"icon-gen": "mkdirp icon-build build-resources/appx && node script/icon-gen.mjs",
"download-ffmpeg-darwin-x64": "mkdirp ffmpeg/darwin-x64 && cd ffmpeg/darwin-x64 && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffmpeg-macos-X64 -O ffmpeg && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffprobe-macos-X64 -O ffprobe && chmod +x ffmpeg && chmod +x ffprobe",
"download-ffmpeg-darwin-arm64": "mkdirp ffmpeg/darwin-arm64 && cd ffmpeg/darwin-arm64 && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffmpeg-macos-ARM64 -O ffmpeg && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffprobe-macos-ARM64 -O ffprobe && chmod +x ffmpeg && chmod +x ffprobe",
@ -18,7 +18,7 @@
"build": "yarn icon-gen && vite build --outDir vite-dist",
"tsc": "tsc --build",
"test": "vitest",
"lint": "eslint --ext .jsx --ext .js . --ext .mjs",
"lint": "eslint --ext .js,.ts,.jsx,.tsx,.mjs .",
"pack-mac": "electron-builder --mac -m dmg",
"prepack-mac": "yarn build",
"pack-mas-dev": "electron-builder --mac -m mas-dev -c.mas.provisioningProfile=LosslessCut_Dev.provisionprofile -c.mas.identity='Apple Development: Mikael Finstad (JH4PH8B3C8)'",
@ -47,10 +47,11 @@
"@radix-ui/react-switch": "^1.0.1",
"@tsconfig/strictest": "^2.0.2",
"@tsconfig/vite-react": "^3.0.0",
"@types/eslint": "^8",
"@types/lodash": "^4.14.202",
"@types/sortablejs": "^1.15.0",
"@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.17.0",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"@uidotdev/usehooks": "^2.4.1",
"@vitejs/plugin-react": "^3.1.0",
"color": "^3.1.0",
@ -61,13 +62,13 @@
"electron": "^27.0.0",
"electron-builder": "^24.6.4",
"electron-devtools-installer": "^3.2.0",
"eslint": "^8.53.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint": "^8.2.0",
"eslint-config-mifi": "^0.0.3",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-unicorn": "^51.0.1",
"evergreen-ui": "^6.13.1",
"fast-xml-parser": "^4.2.5",
"framer-motion": "^9.0.3",
@ -97,7 +98,7 @@
"sortablejs": "^1.13.0",
"sweetalert2": "^11.0.0",
"sweetalert2-react-content": "^5.0.7",
"typescript": "^5.3.3",
"typescript": "~5.2.0",
"typescript-plugin-css-modules": "^5.1.0",
"use-debounce": "^5.1.0",
"use-trace-update": "^1.3.0",
@ -132,9 +133,6 @@
"winston": "^3.8.1",
"yargs-parser": "^21.0.0"
},
"eslintConfig": {
"extends": "react-app"
},
"build": {
"directories": {
"buildResources": "build-resources"

View File

@ -7,6 +7,7 @@ function createMediaSourceStream({ path, videoStreamIndex, audioStreamIndex, see
logger.info('Starting preview process', { videoStreamIndex, audioStreamIndex, seekTo });
const process = createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, seekTo, size, fps });
// eslint-disable-next-line unicorn/prefer-add-event-listener
abortController.signal.onabort = () => {
logger.info('Aborting preview process', { videoStreamIndex, audioStreamIndex, seekTo });
process.kill('SIGKILL');
@ -64,7 +65,7 @@ function createMediaSourceStream({ path, videoStreamIndex, audioStreamIndex, see
if (!err.killed) {
console.warn(err.message);
console.warn(stderr.toString('utf-8'));
console.warn(stderr.toString('utf8'));
}
}
})();
@ -76,6 +77,7 @@ function readOneJpegFrameWrapper({ path, seekTo, videoStreamIndex }) {
const abortController = new AbortController();
const process = readOneJpegFrame({ path, seekTo, videoStreamIndex });
// eslint-disable-next-line unicorn/prefer-add-event-listener
abortController.signal.onabort = () => process.kill('SIGKILL');
function abort() {

View File

@ -1,4 +1,5 @@
const Store = require('electron-store');
// eslint-disable-next-line import/no-extraneous-dependencies
const electron = require('electron');
const os = require('os');
const { join, dirname } = require('path');
@ -175,7 +176,7 @@ async function tryCreateStore({ customStoragePath }) {
return;
} catch (err) {
// eslint-disable-next-line no-await-in-loop
await new Promise(r => setTimeout(r, 2000));
await new Promise((r) => setTimeout(r, 2000));
logger.error('Failed to create config store, retrying', err);
}
}
@ -198,12 +199,10 @@ async function init() {
}
const cleanupChoices = store.get('cleanupChoices'); // todo remove after a while
if (cleanupChoices != null) {
if (cleanupChoices.closeFile == null) {
if (cleanupChoices != null && cleanupChoices.closeFile == null) {
logger.info('Migrating cleanupChoices.closeFile');
set('cleanupChoices', { ...cleanupChoices, closeFile: true });
}
}
}
module.exports = {

View File

@ -1,3 +1,4 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { Menu } = require('electron');
// https://github.com/electron/electron/issues/4068#issuecomment-274159726

View File

@ -1,6 +1,7 @@
process.traceDeprecation = true;
process.traceProcessWarnings = true;
// eslint-disable-next-line import/no-extraneous-dependencies
const electron = require('electron');
const isDev = require('electron-is-dev');
const unhandled = require('electron-unhandled');
@ -14,7 +15,7 @@ const { stat } = require('fs/promises');
const logger = require('./logger');
const menu = require('./menu');
const configStore = require('./configStore');
const { frontendBuildDir, isLinux, isWindows } = require('./util');
const { frontendBuildDir, isLinux } = require('./util');
const attachContextMenu = require('./contextMenu');
const HttpServer = require('./httpServer');
@ -298,6 +299,7 @@ function initApp() {
// Call this immediately, to make sure we don't miss it (race condition)
const readyPromise = app.whenReady();
// eslint-disable-next-line unicorn/prefer-top-level-await
(async () => {
try {
logger.info('Initializing config store');
@ -343,8 +345,8 @@ const readyPromise = app.whenReady();
const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer'); // eslint-disable-line global-require,import/no-extraneous-dependencies
installExtension(REACT_DEVELOPER_TOOLS)
.then(name => logger.info('Added Extension', name))
.catch(err => logger.error('Failed to add extension', err));
.then((name) => logger.info('Added Extension', name))
.catch((err) => logger.error('Failed to add extension', err));
}
createWindow();

View File

@ -18,8 +18,7 @@ function setCustomFfPath(path) {
}
function getFfCommandLine(cmd, args) {
const mapArg = arg => (/[^0-9a-zA-Z-_]/.test(arg) ? `'${arg}'` : arg);
return `${cmd} ${args.map(mapArg).join(' ')}`;
return `${cmd} ${args.map((arg) => (/[^\w-]/.test(arg) ? `'${arg}'` : arg)).join(' ')}`;
}
function getFfPath(cmd) {
@ -56,8 +55,10 @@ function handleProgress(process, durationIn, onProgress, customMatcher = () => u
// console.log('progress', line);
try {
// eslint-disable-next-line unicorn/better-regex
let match = line.match(/frame=\s*[^\s]+\s+fps=\s*[^\s]+\s+q=\s*[^\s]+\s+(?:size|Lsize)=\s*[^\s]+\s+time=\s*([^\s]+)\s+/);
// Audio only looks like this: "line size= 233422kB time=01:45:50.68 bitrate= 301.1kbits/s speed= 353x "
// eslint-disable-next-line unicorn/better-regex
if (!match) match = line.match(/(?:size|Lsize)=\s*[^\s]+\s+time=\s*([^\s]+)\s+/);
if (!match) {
customMatcher(line);
@ -108,7 +109,7 @@ function runFfmpegProcess(args, customExecaOptions, { logCli = true } = {}) {
runningFfmpegs.add(process);
try {
await process;
} catch (err) {
} catch {
// ignored here
} finally {
runningFfmpegs.delete(process);
@ -246,10 +247,11 @@ async function detectSceneChanges({ filePath, minChange, onProgress, from, to })
handleProgress(process, to - from, onProgress);
const rl = readline.createInterface({ input: process.stdout });
rl.on('line', (line) => {
// eslint-disable-next-line unicorn/better-regex
const match = line.match(/^frame:\d+\s+pts:\d+\s+pts_time:([\d.]+)/);
if (!match) return;
const time = parseFloat(match[1]);
if (Number.isNaN(time) || time <= times[times.length - 1]) return;
if (Number.isNaN(time) || time <= times.at(-1)) return;
times.push(time);
});
@ -288,6 +290,7 @@ const mapFilterOptions = (options) => Object.entries(options).map(([key, value])
async function blackDetect({ filePath, filterOptions, onProgress, from, to }) {
function matchLineTokens(line) {
// eslint-disable-next-line unicorn/better-regex
const match = line.match(/^[blackdetect\s*@\s*0x[0-9a-f]+] black_start:([\d\\.]+) black_end:([\d\\.]+) black_duration:[\d\\.]+/);
if (!match) return {};
return {
@ -301,6 +304,7 @@ async function blackDetect({ filePath, filterOptions, onProgress, from, to }) {
async function silenceDetect({ filePath, filterOptions, onProgress, from, to }) {
function matchLineTokens(line) {
// eslint-disable-next-line unicorn/better-regex
const match = line.match(/^[silencedetect\s*@\s*0x[0-9a-f]+] silence_end: ([\d\\.]+)[|\s]+silence_duration: ([\d\\.]+)/);
if (!match) return {};
const end = parseFloat(match[1]);
@ -409,6 +413,7 @@ async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVi
switch (video) {
case 'hq': {
// eslint-disable-next-line unicorn/prefer-ternary
if (isMac) {
videoArgs = ['-vf', 'format=yuv420p', '-allow_sw', '1', '-vcodec', 'h264', '-b:v', '15M'];
} else {
@ -423,6 +428,7 @@ async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVi
break;
}
case 'lq': {
// eslint-disable-next-line unicorn/prefer-ternary
if (isMac) {
videoArgs = ['-vf', `scale=-2:${targetHeight},format=yuv420p`, '-allow_sw', '1', '-sws_flags', 'lanczos', '-vcodec', 'h264', '-b:v', '1500k'];
} else {
@ -443,6 +449,7 @@ async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVi
switch (audio) {
case 'hq': {
// eslint-disable-next-line unicorn/prefer-ternary
if (isMac) {
audioArgs = ['-acodec', 'aac_at', '-b:a', '192k'];
} else {
@ -451,6 +458,7 @@ async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVi
break;
}
case 'lq': {
// eslint-disable-next-line unicorn/prefer-ternary
if (isMac) {
audioArgs = ['-acodec', 'aac_at', '-ar', '44100', '-ac', '2', '-b:a', '96k'];
} else {

View File

@ -1,6 +1,8 @@
// eslint-disable-line unicorn/filename-case
// intentionally disabled because I don't know the quality of the languages, so better to default to english
// const LanguageDetector = window.require('i18next-electron-language-detector');
const isDev = require('electron-is-dev');
// eslint-disable-next-line import/no-extraneous-dependencies
const { app } = require('electron');
const { join } = require('path');

View File

@ -1,6 +1,7 @@
const winston = require('winston');
const util = require('util');
const isDev = require('electron-is-dev');
// eslint-disable-next-line import/no-extraneous-dependencies
const { app } = require('electron');
const { join } = require('path');

View File

@ -1,9 +1,10 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const electron = require('electron');
const { t } = require('i18next');
// menu-safe i18n.t:
// https://github.com/mifi/lossless-cut/issues/1456
const esc = (val) => val.replace(/&/g, '&&');
const esc = (val) => val.replaceAll('&', '&&');
const { Menu } = electron;

View File

@ -1,3 +1,4 @@
// eslint-disable-line unicorn/filename-case
const GitHub = require('github-api');
// eslint-disable-next-line import/no-extraneous-dependencies
const electron = require('electron');

View File

@ -1,3 +1,4 @@
// eslint-disable-line unicorn/filename-case
import sharp from 'sharp';
import icongen from 'icon-gen';

View File

@ -1,3 +1,4 @@
// eslint-disable-line unicorn/filename-case
import execa from 'execa';
import { readFile } from 'fs/promises';

View File

@ -101,6 +101,7 @@ const videoStyle: CSSProperties = { width: '100%', height: '100%', objectFit: 'c
const bottomStyle: CSSProperties = { background: controlsBackground, transition: darkModeTransition };
const hevcPlaybackSupportedPromise = doesPlayerSupportHevcPlayback();
// eslint-disable-next-line unicorn/prefer-top-level-await
hevcPlaybackSupportedPromise.catch((err) => console.error(err));
@ -256,7 +257,7 @@ function App() {
});
}, []);
const toggleSegmentsList = useCallback(() => setShowRightBar(v => !v), []);
const toggleSegmentsList = useCallback(() => setShowRightBar((v) => !v), []);
const toggleCopyStreamId = useCallback((path, index) => {
setCopyStreamIdsForPath(path, (old) => ({ ...old, [index]: !old[index] }));
@ -333,7 +334,7 @@ function App() {
!!(copyStreamIdsByFile[path] || {})[streamId]
), [copyStreamIdsByFile]);
const checkCopyingAnyTrackOfType = useCallback((filter) => mainStreams.some(stream => isCopyingStreamId(filePath, stream.index) && filter(stream)), [filePath, isCopyingStreamId, mainStreams]);
const checkCopyingAnyTrackOfType = useCallback((filter) => mainStreams.some((stream) => isCopyingStreamId(filePath, stream.index) && filter(stream)), [filePath, isCopyingStreamId, mainStreams]);
const copyAnyAudioTrack = useMemo(() => checkCopyingAnyTrackOfType((stream) => stream.codec_type === 'audio'), [checkCopyingAnyTrackOfType]);
const subtitleStreams = useMemo(() => getSubtitleStreams(mainStreams), [mainStreams]);
@ -633,7 +634,7 @@ function App() {
}, []);
const mainCopiedStreams = useMemo(() => mainStreams.filter((stream) => isCopyingStreamId(filePath, stream.index)), [filePath, isCopyingStreamId, mainStreams]);
const mainCopiedThumbnailStreams = useMemo(() => mainCopiedStreams.filter(isStreamThumbnail), [mainCopiedStreams]);
const mainCopiedThumbnailStreams = useMemo(() => mainCopiedStreams.filter((stream) => isStreamThumbnail(stream)), [mainCopiedStreams]);
// Streams that are not copy enabled by default
const extraStreams = useMemo(() => mainStreams.filter((stream) => !shouldCopyStreamByDefault(stream)), [mainStreams]);
@ -678,7 +679,7 @@ function App() {
function addThumbnail(thumbnail) {
// console.log('Rendered thumbnail', thumbnail.url);
setThumbnails(v => [...v, thumbnail]);
setThumbnails((v) => [...v, thumbnail]);
}
const hasAudio = !!activeAudioStream;
@ -710,7 +711,7 @@ function App() {
// Cleanup removed thumbnails
useEffect(() => {
thumnailsRef.current.forEach((thumbnail) => {
if (!thumbnails.some(t => t.url === thumbnail.url)) URL.revokeObjectURL(thumbnail.url);
if (!thumbnails.some((t) => t.url === thumbnail.url)) URL.revokeObjectURL(thumbnail.url);
});
thumnailsRef.current = thumbnails;
}, [thumbnails]);
@ -719,7 +720,7 @@ function App() {
const subtitlesByStreamIdRef = useRef({});
useEffect(() => {
Object.values(thumnailsRef.current).forEach(({ url }) => {
if (!Object.values(subtitlesByStreamId).some(t => t.url === url)) URL.revokeObjectURL(url);
if (!Object.values(subtitlesByStreamId).some((t) => t.url === url)) URL.revokeObjectURL(url);
});
subtitlesByStreamIdRef.current = subtitlesByStreamId;
}, [subtitlesByStreamId]);
@ -732,7 +733,7 @@ function App() {
const resetMergedOutFileName = useCallback(() => {
const ext = getOutFileExtension({ isCustomFormatSelected, outFormat: fileFormat, filePath });
const outFileName = getSuffixedFileName(filePath, `cut-merged-${new Date().getTime()}${ext}`);
const outFileName = getSuffixedFileName(filePath, `cut-merged-${Date.now()}${ext}`);
setMergedOutFileName(outFileName);
}, [fileFormat, filePath, isCustomFormatSelected]);
@ -833,7 +834,7 @@ function App() {
}, [html5ify, html5ifyDummy]);
const convertFormatBatch = useCallback(async () => {
if (batchFiles.length < 1) return;
if (batchFiles.length === 0) return;
const filePaths = batchFiles.map((f) => f.path);
const failedFiles: string[] = [];
@ -1008,7 +1009,7 @@ function App() {
externalFilesMeta,
mainStreams,
copyStreamIdsByFile,
cutSegments: cutSegments.map(s => ({ start: s.start, end: s.end })),
cutSegments: cutSegments.map((s) => ({ start: s.start, end: s.end })),
mainFileFormatData,
rotation,
shortestFlag,
@ -1176,7 +1177,7 @@ function App() {
return;
}
if (segmentsToExport.length < 1) {
if (segmentsToExport.length === 0) {
return;
}
@ -1526,6 +1527,7 @@ function App() {
await html5ifyAndLoadWithPreferences(cod, fp, 'fastest', haveVideoStream, haveAudioStream);
}
// eslint-disable-next-line unicorn/prefer-ternary
if (projectPath) {
await loadEdlFile({ path: projectPath, type: 'llc' });
} else {
@ -1565,16 +1567,14 @@ function App() {
// https://github.com/mifi/lossless-cut/issues/515
setFilePath(fp);
} catch (err) {
if (err) {
if (err instanceof DirectoryAccessDeclinedError) return;
}
resetState();
throw err;
}
}, [storeProjectInWorkingDir, setWorking, loadEdlFile, getEdlFilePath, getEdlFilePathOld, enableAskForImportChapters, ensureAccessToSourceDir, loadCutSegments, autoLoadTimecode, enableNativeHevc, ensureWritableOutDir, customOutDir, resetState, setCopyStreamIdsForPath, setFileFormat, outFormatLocked, setDetectedFileFormat, html5ifyAndLoadWithPreferences, showPreviewFileLoadedMessage, showUnsupportedFileMessage, hideAllNotifications]);
const toggleLastCommands = useCallback(() => setLastCommandsVisible(val => !val), []);
const toggleSettings = useCallback(() => setSettingsVisible(val => !val), []);
const toggleLastCommands = useCallback(() => setLastCommandsVisible((val) => !val), []);
const toggleSettings = useCallback(() => setSettingsVisible((val) => !val), []);
const seekClosestKeyframe = useCallback((direction) => {
const time = findNearestKeyFrameTime({ time: getRelevantTime(), direction });
@ -1822,7 +1822,7 @@ function App() {
const userOpenFiles = useCallback(async (filePathsIn) => {
let filePaths = filePathsIn;
if (!filePaths || filePaths.length < 1) return;
if (!filePaths || filePaths.length === 0) return;
console.log('userOpenFiles');
console.log(filePaths.join('\n'));
@ -1866,7 +1866,7 @@ function App() {
const firstFilePath = filePaths[0];
// https://en.wikibooks.org/wiki/Inside_DVD-Video/Directory_Structure
if (/^VIDEO_TS$/i.test(basename(firstFilePath))) {
if (/^video_ts$/i.test(basename(firstFilePath))) {
if (mustDisallowVob()) return;
filePaths = await readVideoTs(firstFilePath);
}
@ -1975,7 +1975,7 @@ function App() {
const showIncludeExternalStreamsDialog = useCallback(async () => {
try {
const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile'] });
if (canceled || filePaths.length < 1) return;
if (canceled || filePaths.length === 0) return;
await addStreamSourceFile(filePaths[0]);
} catch (err) {
handleError(err);
@ -2031,9 +2031,9 @@ function App() {
play: () => play(),
pause,
reducePlaybackRate: () => changePlaybackRate(-1),
reducePlaybackRateMore: () => changePlaybackRate(-1, 2.0),
reducePlaybackRateMore: () => changePlaybackRate(-1, 2),
increasePlaybackRate: () => changePlaybackRate(1),
increasePlaybackRateMore: () => changePlaybackRate(1, 2.0),
increasePlaybackRateMore: () => changePlaybackRate(1, 2),
timelineToggleComfortZoom,
captureSnapshot,
captureSnapshotAsCoverArt,
@ -2191,7 +2191,9 @@ function App() {
useKeyboard({ keyBindings, onKeyPress });
useEffect(() => {
// eslint-disable-next-line unicorn/prefer-add-event-listener
document.ondragover = dragPreventer;
// eslint-disable-next-line unicorn/prefer-add-event-listener
document.ondragend = dragPreventer;
electron.ipcRenderer.send('renderer-ready');
@ -2309,7 +2311,7 @@ function App() {
// todo
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const actionsWithArgs: Record<string, (...args: any[]) => void> = {
openFiles: (filePaths: string[]) => { userOpenFiles(filePaths.map(resolvePathIfNeeded)); },
openFiles: (filePaths: string[]) => { userOpenFiles(filePaths.map((p) => resolvePathIfNeeded(p))); },
// todo separate actions per type and move them into mainActions? https://github.com/mifi/lossless-cut/issues/254#issuecomment-932649424
importEdlFile,
exportEdlFile: tryExportEdlFile,
@ -2351,7 +2353,7 @@ function App() {
ev.preventDefault();
if (!ev.dataTransfer) return;
const { files } = ev.dataTransfer;
const filePaths = Array.from(files).map(f => f.path);
const filePaths = [...files].map((f) => f.path);
focusWindow();
@ -2382,7 +2384,7 @@ function App() {
useEffect(() => {
const keyScrollPreventer = (e) => {
// https://stackoverflow.com/questions/8916620/disable-arrow-key-scrolling-in-users-browser
if (e.target === document.body && [32, 37, 38, 39, 40].indexOf(e.keyCode) > -1) {
if (e.target === document.body && [32, 37, 38, 39, 40].includes(e.keyCode)) {
e.preventDefault();
}
};
@ -2393,7 +2395,7 @@ function App() {
const showLeftBar = batchFiles.length > 0;
const thumbnailsSorted = useMemo(() => sortBy(thumbnails, thumbnail => thumbnail.time), [thumbnails]);
const thumbnailsSorted = useMemo(() => sortBy(thumbnails, (thumbnail) => thumbnail.time), [thumbnails]);
const { t } = useTranslation();

View File

@ -1,4 +1,4 @@
import React, { memo } from 'react';
import { memo } from 'react';
import { motion } from 'framer-motion';
import { FaTrashAlt, FaSave } from 'react-icons/fa';

View File

@ -27,7 +27,7 @@ import { askForPlaybackRate } from './dialogs';
const { clipboard } = window.require('electron');
const zoomOptions = Array(13).fill().map((unused, z) => 2 ** z);
const zoomOptions = Array.from({ length: 13 }).fill().map((unused, z) => 2 ** z);
const leftRightWidth = 100;
@ -35,7 +35,7 @@ const InvertCutModeButton = memo(({ invertCutSegments, setInvertCutSegments }) =
const { t } = useTranslation();
const onYinYangClick = useCallback(() => {
setInvertCutSegments(v => {
setInvertCutSegments((v) => {
const newVal = !v;
if (newVal) toast.fire({ title: t('When you export, selected segments on the timeline will be REMOVED - the surrounding areas will be KEPT') });
else toast.fire({ title: t('When you export, selected segments on the timeline will be KEPT - the surrounding areas will be REMOVED.') });
@ -154,7 +154,7 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see
style={{ ...cutTimeInputStyle, color: isCutTimeManualSet() ? 'var(--red11)' : 'var(--gray12)' }}
type="text"
title={isStart ? t('Manually input current segment\'s start time') : t('Manually input current segment\'s end time')}
onChange={e => handleCutTimeInput(e.target.value)}
onChange={(e) => handleCutTimeInput(e.target.value)}
onPaste={handleCutTimePaste}
onBlur={() => setCutTimeManual()}
onContextMenu={handleContextMenu}
@ -392,9 +392,9 @@ const BottomBar = memo(({
<div role="button" style={{ marginRight: 5, marginLeft: 10 }} title={t('Zoom')} onClick={timelineToggleComfortZoom}>{Math.floor(zoom)}x</div>
<Select style={{ height: 20, flexBasis: 85, flexGrow: 0 }} value={zoomOptions.includes(zoom) ? zoom.toString() : ''} title={t('Zoom')} onChange={withBlur(e => setZoom(parseInt(e.target.value, 10)))}>
<Select style={{ height: 20, flexBasis: 85, flexGrow: 0 }} value={zoomOptions.includes(zoom) ? zoom.toString() : ''} title={t('Zoom')} onChange={withBlur((e) => setZoom(parseInt(e.target.value, 10)))}>
<option key="" value="" disabled>{t('Zoom')}</option>
{zoomOptions.map(val => (
{zoomOptions.map((val) => (
<option key={val} value={String(val)}>{t('Zoom')} {val}x</option>
))}
</Select>

View File

@ -94,7 +94,7 @@ async function startPlayback({ path, video, videoStreamIndex, audioStreamIndex,
return undefined;
}
if (sourceBuffer.buffered.length < 1) {
if (sourceBuffer.buffered.length === 0) {
return undefined;
}
@ -239,7 +239,9 @@ function drawJpegFrame(canvas: HTMLCanvasElement, jpegImage: Buffer) {
console.error('Canvas context is null');
return;
}
// eslint-disable-next-line unicorn/prefer-add-event-listener
img.onload = () => ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// eslint-disable-next-line unicorn/prefer-add-event-listener
img.onerror = (error) => console.error('Canvas JPEG image error', error);
img.src = `data:image/jpeg;base64,${jpegImage.toString('base64')}`;
}

View File

@ -1,4 +1,4 @@
import React, { memo, useMemo, useRef, useCallback, useState } from 'react';
import { memo, useMemo, useRef, useCallback, useState } from 'react';
import { FaYinYang, FaSave, FaPlus, FaMinus, FaTag, FaSortNumericDown, FaAngleRight, FaRegCheckCircle, FaRegCircle } from 'react-icons/fa';
import { AiOutlineSplitCells } from 'react-icons/ai';
import { motion } from 'framer-motion';
@ -186,13 +186,9 @@ const SegmentList = memo(({
let header = t('Segments to export:');
if (segments.length === 0) {
if (invertCutSegments) {
header = (
header = invertCutSegments ? (
<Trans>You have enabled the &quot;invert segments&quot; mode <FaYinYang style={{ verticalAlign: 'middle' }} /> which will cut away selected segments instead of keeping them. But there is no space between any segments, or at least two segments are overlapping. This would not produce any output. Either make room between segments or click the Yinyang <FaYinYang style={{ verticalAlign: 'middle', color: primaryTextColor }} /> symbol below to disable this mode. Alternatively you may combine overlapping segments from the menu.</Trans>
);
} else {
header = t('No segments to export.');
}
) : t('No segments to export.');
}
const onReorderSegs = useCallback(async (index) => {
@ -285,6 +281,7 @@ const SegmentList = memo(({
[tag]: value,
})), [setEditingSegmentTags]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const onTagReset = useCallback((tag) => setEditingSegmentTags(({ [tag]: deleted, ...rest }) => rest), [setEditingSegmentTags]);
const onSegmentTagsCloseComplete = useCallback(() => {

View File

@ -1,4 +1,4 @@
import React, { memo, useState, useMemo, useCallback } from 'react';
import { memo, useState, useMemo, useCallback } from 'react';
import { FaImage, FaCheckCircle, FaPaperclip, FaVideo, FaVideoSlash, FaFileImport, FaVolumeUp, FaVolumeMute, FaBan, FaFileExport } from 'react-icons/fa';
import { GoFileBinary } from 'react-icons/go';
@ -34,6 +34,7 @@ const EditFileDialog = memo(({ editingFile, allFilesMeta, customTagsByFile, setC
const onTagReset = useCallback((tag) => {
setCustomTagsByFile((old) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [tag]: deleted, ...rest } = old[editingFile] || {};
return { ...old, [editingFile]: rest };
});
@ -101,7 +102,8 @@ const EditStreamDialog = memo(({ editingStream: { streamId: editingStreamId, pat
const onTagReset = useCallback((tag) => {
updateStreamParams(editingFile, editingStreamId, (params) => {
if (!params.has('customTags')) return;
// eslint-disable-next-line no-param-reassign
// todo
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-dynamic-delete
delete params.get('customTags')[tag];
});
}, [editingFile, editingStreamId, updateStreamParams]);
@ -140,6 +142,7 @@ const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copySt
let Icon;
let codecTypeHuman;
// eslint-disable-next-line unicorn/prefer-switch
if (stream.codec_type === 'audio') {
Icon = copyStream ? FaVolumeUp : FaVolumeMute;
codecTypeHuman = t('audio');
@ -310,6 +313,7 @@ const StreamsSelector = memo(({
async function removeFile(path) {
setCopyStreamIdsForPath(path, () => ({}));
setExternalFilesMeta((old) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [path]: val, ...rest } = old;
return rest;
});
@ -318,6 +322,7 @@ const StreamsSelector = memo(({
async function batchSetCopyStreamIdsForPath(path, streams, filter, enabled) {
setCopyStreamIdsForPath(path, (old) => {
const ret = { ...old };
// eslint-disable-next-line unicorn/no-array-callback-reference
streams.filter(filter).forEach(({ index }) => {
ret[index] = enabled;
});

View File

@ -245,6 +245,7 @@ const Timeline = memo(({
style={{ position: 'relative', borderTop: '1px solid var(--gray7)', borderBottom: '1px solid var(--gray7)' }}
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
// eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
onMouseOut={onMouseOut}
>
{(waveformEnabled && !shouldShowWaveform) && (

View File

@ -1,7 +1,7 @@
import React, { memo, useCallback } from 'react';
import { IoIosSettings } from 'react-icons/io';
import { FaLock, FaUnlock } from 'react-icons/fa';
import { IconButton, CrossIcon, ListIcon, VolumeUpIcon, VolumeOffIcon } from 'evergreen-ui';
import { CrossIcon, ListIcon, VolumeUpIcon, VolumeOffIcon } from 'evergreen-ui';
import { useTranslation } from 'react-i18next';
import Button from './components/Button';

View File

@ -52,14 +52,14 @@ const BigWaveform = memo(({ waveforms, relevantTime, playing, durationSafe, zoom
useEffect(() => {
const startTime = new Date().getTime();
const startTime = Date.now();
if (playing) {
let raf;
// eslint-disable-next-line no-inner-declarations
function render() {
raf = window.requestAnimationFrame(() => {
setSmoothTime(relevantTime + (new Date().getTime() - startTime) / 1000);
setSmoothTime(relevantTime + (Date.now() - startTime) / 1000);
render();
});
}

View File

@ -25,12 +25,16 @@ const rowStyle: CSSProperties = {
};
const ConcatDialog = memo(({ isShown, onHide, paths, onConcat, alwaysConcatMultipleFiles, setAlwaysConcatMultipleFiles }: {
// todo
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isShown: boolean, onHide: () => void, paths: string[], onConcat: (a: { paths: string[], includeAllStreams: boolean, streams: any, outFileName: string, fileFormat: string, clearBatchFilesAfterConcat: boolean }) => Promise<void>, alwaysConcatMultipleFiles: boolean, setAlwaysConcatMultipleFiles: (a: boolean) => void,
}) => {
const { t } = useTranslation();
const { preserveMovData, setPreserveMovData, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge } = useUserSettings();
const [includeAllStreams, setIncludeAllStreams] = useState(false);
// todo
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [fileMeta, setFileMeta] = useState<{ format: any, streams: any, chapters: any }>();
const [allFilesMetaCache, setAllFilesMetaCache] = useState({});
const [clearBatchFilesAfterConcat, setClearBatchFilesAfterConcat] = useState(false);
@ -62,7 +66,7 @@ const ConcatDialog = memo(({ isShown, onHide, paths, onConcat, alwaysConcatMulti
setFileMeta(fileMetaNew);
setFileFormat(fileFormatNew);
setDetectedFileFormat(fileFormatNew);
setUniqueSuffix(new Date().getTime());
setUniqueSuffix(Date.now());
})().catch(console.error);
return () => {

View File

@ -24,7 +24,7 @@ const renderKeys = (keys) => keys.map((key, i) => (
// From https://craig.is/killing/mice
// For modifier keys you can use shift, ctrl, alt, or meta.
// You can substitute option for alt and command for meta.
const allModifiers = ['shift', 'ctrl', 'alt', 'meta'];
const allModifiers = new Set(['shift', 'ctrl', 'alt', 'meta']);
function fixKeys(keys) {
const replaced = keys.map((key) => {
if (key === 'option') return 'alt';
@ -32,10 +32,10 @@ function fixKeys(keys) {
return key;
});
const uniqed = uniq(replaced);
const nonModifierKeys = keys.filter((key) => !allModifiers.includes(key));
const nonModifierKeys = keys.filter((key) => !allModifiers.has(key));
if (nonModifierKeys.length === 0) return []; // only modifiers is invalid
if (nonModifierKeys.length > 1) return []; // can only have one non-modifier
return orderBy(uniqed, [key => key !== 'shift', key => key !== 'ctrl', key => key !== 'alt', key => key !== 'meta', key => key]);
return orderBy(uniqed, [(key) => key !== 'shift', (key) => key !== 'ctrl', (key) => key !== 'alt', (key) => key !== 'meta', (key) => key]);
}
const CreateBinding = memo(({

View File

@ -27,7 +27,7 @@ const OutputFormatSelect = memo(({ style, detectedFileFormat, fileFormat, onOutp
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<Select style={style} value={fileFormat || ''} title={i18n.t('Output container format:')} onChange={withBlur(e => onOutputFormatUserChange(e.target.value))}>
<Select style={style} value={fileFormat || ''} title={i18n.t('Output container format:')} onChange={withBlur((e) => onOutputFormatUserChange(e.target.value))}>
<option key="disabled1" value="" disabled>{i18n.t('Output container format:')}</option>
{detectedFileFormat && (

View File

@ -222,7 +222,8 @@ async function askForSegmentDuration(fileDuration) {
// https://github.com/mifi/lossless-cut/issues/1153
async function askForSegmentsRandomDurationRange() {
function parse(str) {
const match = str.replace(/\s/g, '').match(/^duration([\d.]+)to([\d.]+),gap([-\d.]+)to([-\d.]+)$/i);
// eslint-disable-next-line unicorn/better-regex
const match = str.replaceAll(/\s/g, '').match(/^duration([\d.]+)to([\d.]+),gap([-\d.]+)to([-\d.]+)$/i);
if (!match) return undefined;
const values = match.slice(1);
const parsed = values.map((val) => parseFloat(val));
@ -275,7 +276,7 @@ export async function askForShiftSegments() {
let parseableValue = value;
let sign = 1;
if (parseableValue[0] === '-') {
parseableValue = parseableValue.substring(1);
parseableValue = parseableValue.slice(1);
sign = -1;
}
const duration = parseDuration(parseableValue);

View File

@ -9,7 +9,7 @@ import { parseSrt, formatSrt, parseYouTube, formatYouTube, parseMplayerEdl, pars
// eslint-disable-next-line no-underscore-dangle
const __dirname = dirname(fileURLToPath(import.meta.url));
const readFixture = async (name: string, encoding: BufferEncoding = 'utf-8') => fs.readFile(join(__dirname, 'fixtures', name), encoding);
const readFixture = async (name: string, encoding: BufferEncoding = 'utf8') => fs.readFile(join(__dirname, 'fixtures', name), encoding);
const readFixtureBinary = async (name: string) => fs.readFile(join(__dirname, 'fixtures', name), null);
const expectYouTube1 = [
@ -259,5 +259,5 @@ it('format srt', async () => {
// https://github.com/mifi/lossless-cut/issues/1664
it('parses DV Analyzer Summary.txt', async () => {
expect(parseDvAnalyzerSummaryTxt(await readFixture('DV Analyzer Summary.txt', 'utf-8'))).toMatchSnapshot();
expect(parseDvAnalyzerSummaryTxt(await readFixture('DV Analyzer Summary.txt', 'utf8'))).toMatchSnapshot();
});

View File

@ -21,10 +21,10 @@ export function getFrameCountRaw(detectedFps: number | undefined, sec: number) {
}
function parseTime(str: string) {
const timeMatch = str.match(/^[^0-9]*(?:(?:([0-9]{1,}):)?([0-9]{1,2}):)?([0-9]{1,})(?:\.([0-9]{1,3}))?:?/);
const timeMatch = str.match(/^\D*(?:(?:(\d+):)?(\d{1,2}):)?(\d+)(?:\.(\d{1,3}))?:?/);
if (!timeMatch) return undefined;
const rest = str.substring(timeMatch[0].length);
const rest = str.slice(timeMatch[0].length);
const [, hourStr, minStr, secStr, msStr] = timeMatch;
const hour = hourStr != null ? parseInt(hourStr, 10) : 0;
@ -49,7 +49,7 @@ export const getFrameValParser = (fps) => (str) => {
export async function parseCsv(csvStr, parseTimeFn) {
const rows = await csvParseAsync(csvStr, {});
if (rows.length === 0) throw new Error(i18n.t('No rows found'));
if (!rows.every(row => row.length === 3)) throw new Error(i18n.t('One or more rows does not have 3 columns'));
if (!rows.every((row) => row.length === 3)) throw new Error(i18n.t('One or more rows does not have 3 columns'));
const mapped = rows
.map(([start, end, name]) => ({
@ -71,7 +71,7 @@ export async function parseCsv(csvStr, parseTimeFn) {
export async function parseMplayerEdl(text) {
const allRows = text.split('\n').map((line) => {
const match = line.match(/^\s*([^\s]+)\s+([^\s]+)\s+([0123])\s*$/);
const match = line.match(/^\s*(\S+)\s+(\S+)\s+([0-3])\s*$/);
if (!match) return undefined;
const start = parseFloat(match[1]);
const end = parseFloat(match[2]);
@ -79,7 +79,7 @@ export async function parseMplayerEdl(text) {
if (Number.isNaN(start) || Number.isNaN(end)) return undefined;
if (start < 0 || end < 0 || start >= end) return undefined;
return { start, end, type };
}).filter((it) => it);
}).filter(Boolean);
const cutAwaySegments = allRows.filter((row) => row.type === 0);
const muteSegments = allRows.filter((row) => row.type === 1);
@ -128,10 +128,10 @@ export function parseCuesheet(cuesheet) {
export function parsePbf(buf: Buffer) {
const text = buf.toString('utf16le');
const bookmarks = text.split('\n').map((line) => {
const match = line.match(/^[0-9]+=([0-9]+)\*([^*]+)*([^*]+)?/);
const match = line.match(/^\d+=(\d+)\*([^*]+)*([^*]+)?/);
if (match) return { time: parseInt(match[1]!, 10) / 1000, name: match[2] };
return undefined;
}).filter((it) => it);
}).filter(Boolean);
const out: Segment[] = [];
@ -156,7 +156,7 @@ export function parseXmeml(xmlStr) {
// TODO maybe support media.audio also?
const { xmeml } = xml;
if (!xmeml) throw Error('Root element <xmeml> not found in file');
if (!xmeml) throw new Error('Root element <xmeml> not found in file');
let sequence;
@ -181,10 +181,10 @@ export function parseFcpXml(xmlStr) {
const xml = new XMLParser({ ignoreAttributes: false }).parse(xmlStr);
const { fcpxml } = xml;
if (!fcpxml) throw Error('Root element <fcpxml> not found in file');
if (!fcpxml) throw new Error('Root element <fcpxml> not found in file');
function getTime(str) {
const match = str.match(/([0-9]+)\/([0-9]+)s/);
const match = str.match(/(\d+)\/(\d+)s/);
if (!match) throw new Error('Invalid attribute');
return parseInt(match[1], 10) / parseInt(match[2], 10);
}
@ -212,7 +212,7 @@ export function parseYouTube(str) {
return { time, name };
}
const lines = str.split('\n').map(parseLine).filter((line) => line);
const lines = str.split('\n').map((line) => parseLine(line)).filter(Boolean);
const linesSorted = sortBy(lines, (l) => l.time);
@ -275,7 +275,7 @@ export function parseDvAnalyzerSummaryTxt(txt: string) {
// eslint-disable-next-line no-restricted-syntax
for (const line of lines) {
if (headerFound) {
const match = line.match(/^(\d{2}):(\d{2}):(\d{2}).(\d{3})\s+([^\s]+)\s+-\s+([^\s]+)\s+([^\s]+\s+[^\s]+)\s+-\s+([^\s]+\s+[^\s]+)/);
const match = line.match(/^(\d{2}):(\d{2}):(\d{2}).(\d{3})\s+(\S+)\s+-\s+(\S+)\s+(\S+\s+\S+)\s+-\s+(\S+\s+\S+)/);
if (!match) break;
const h = parseInt(match[1]!, 10);
const m = parseInt(match[2]!, 10);
@ -325,7 +325,7 @@ export function parseSrt(text: string) {
} else if (subtitleIndexAt != null && subtitleIndexAt > 0) {
const match = line.match(/^(\d+:\d+:\d+[,.]\d+\s+)-->(\s+\d+:\d+:\d+[,.]\d+)$/);
if (match) {
const fixComma = (v) => v.replace(/,/g, '.');
const fixComma = (v) => v.replaceAll(',', '.');
start = parseTime(fixComma(match[1]))?.time;
end = parseTime(fixComma(match[2]))?.time;
} else if (start != null && end != null) {
@ -345,5 +345,5 @@ export function parseSrt(text: string) {
}
export function formatSrt(segments) {
return segments.reduce((acc, segment, index) => `${acc}${index > 0 ? '\r\n' : ''}${index + 1}\r\n${formatDuration({ seconds: segment.start }).replace(/\./g, ',')} --> ${formatDuration({ seconds: segment.end }).replace(/\./g, ',')}\r\n${segment.name || '-'}\r\n`, '');
return segments.reduce((acc, segment, index) => `${acc}${index > 0 ? '\r\n' : ''}${index + 1}\r\n${formatDuration({ seconds: segment.start }).replaceAll('.', ',')} --> ${formatDuration({ seconds: segment.end }).replaceAll('.', ',')}\r\n${segment.name || '-'}\r\n`, '');
}

View File

@ -13,24 +13,24 @@ const { basename } = window.require('path');
const { dialog } = window.require('@electron/remote');
export async function loadCsvSeconds(path) {
return parseCsv(await readFile(path, 'utf-8'), parseCsvTime);
return parseCsv(await readFile(path, 'utf8'), parseCsvTime);
}
export async function loadCsvFrames(path, fps) {
if (!fps) throw new Error('The loaded file has an unknown framerate');
return parseCsv(await readFile(path, 'utf-8'), getFrameValParser(fps));
return parseCsv(await readFile(path, 'utf8'), getFrameValParser(fps));
}
export async function loadXmeml(path) {
return parseXmeml(await readFile(path, 'utf-8'));
return parseXmeml(await readFile(path, 'utf8'));
}
export async function loadFcpXml(path) {
return parseFcpXml(await readFile(path, 'utf-8'));
return parseFcpXml(await readFile(path, 'utf8'));
}
export async function loadDvAnalyzerSummaryTxt(path) {
return parseDvAnalyzerSummaryTxt(await readFile(path, 'utf-8'));
return parseDvAnalyzerSummaryTxt(await readFile(path, 'utf8'));
}
export async function loadPbf(path) {
@ -38,7 +38,7 @@ export async function loadPbf(path) {
}
export async function loadMplayerEdl(path) {
return parseMplayerEdl(await readFile(path, 'utf-8'));
return parseMplayerEdl(await readFile(path, 'utf8'));
}
export async function loadCue(path) {
@ -46,7 +46,7 @@ export async function loadCue(path) {
}
export async function loadSrt(path) {
return parseSrt(await readFile(path, 'utf-8'));
return parseSrt(await readFile(path, 'utf8'));
}
export async function saveCsv(path, cutSegments) {
@ -103,6 +103,7 @@ export async function askForEdlImport({ type, fps }: { type: EdlImportType, fps?
if (type === 'youtube') return askForYouTubeInput();
let filters;
// eslint-disable-next-line unicorn/prefer-switch
if (type === 'csv' || type === 'csv-frames') filters = [{ name: i18n.t('CSV files'), extensions: ['csv'] }];
else if (type === 'xmeml') filters = [{ name: i18n.t('XML files'), extensions: ['xml'] }];
else if (type === 'fcpxml') filters = [{ name: i18n.t('FCPXML files'), extensions: ['fcpxml'] }];
@ -114,7 +115,7 @@ 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 < 1) return [];
if (canceled || filePaths.length === 0) return [];
return readEdlFile({ type, path: filePaths[0], fps });
}
@ -123,6 +124,7 @@ export async function exportEdlFile({ type, cutSegments, customOutDir, filePath,
}) {
let filters;
let ext;
// eslint-disable-next-line unicorn/prefer-switch
if (type === 'csv') {
ext = 'csv';
filters = [{ name: i18n.t('CSV files'), extensions: [ext, 'txt'] }];
@ -148,6 +150,7 @@ export async function exportEdlFile({ type, cutSegments, customOutDir, filePath,
const { canceled, filePath: savePath } = await dialog.showSaveDialog({ defaultPath, filters });
if (canceled || !savePath) return;
console.log('Saving', type, savePath);
// eslint-disable-next-line unicorn/prefer-switch
if (type === 'csv') await saveCsv(savePath, cutSegments);
else if (type === 'tsv-human') await saveTsv(savePath, cutSegments);
else if (type === 'csv-human') await saveCsvHuman(savePath, cutSegments);

View File

@ -1,3 +1,4 @@
// eslint-disable-line unicorn/filename-case
import i18n from 'i18next';
export const blackdetect = () => ({

View File

@ -19,7 +19,12 @@ const { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames
export { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrame, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, html5ify, getDuration, abortFfmpegs, runFfprobe, getFfmpegPath, setCustomFfPath };
export class RefuseOverwriteError extends Error {}
export class RefuseOverwriteError extends Error {
constructor() {
super();
this.name = 'RefuseOverwriteError';
}
}
export function logStdoutStderr({ stdout, stderr }) {
if (stdout.length > 0) {
@ -63,12 +68,12 @@ export async function readFrames({ filePath, from, to, streamIndex }) {
// todo types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const packetsFiltered: Frame[] = (JSON.parse(stdout).packets as any[])
.map(p => ({
.map((p) => ({
keyframe: p.flags[0] === 'K',
time: parseFloat(p.pts_time),
createdAt: new Date(),
}))
.filter(p => !Number.isNaN(p.time));
.filter((p) => !Number.isNaN(p.time));
return sortBy(packetsFiltered, 'time');
}
@ -93,10 +98,18 @@ export type FindKeyframeMode = 'nearest' | 'before' | 'after';
function findKeyframe(keyframes: Keyframe[], time: number, mode: FindKeyframeMode) {
switch (mode) {
case 'nearest': return findNearestKeyframe(keyframes, time);
case 'before': return findPreviousKeyframe(keyframes, time);
case 'after': return findNextKeyframe(keyframes, time);
default: return undefined;
case 'nearest': {
return findNearestKeyframe(keyframes, time);
}
case 'before': {
return findPreviousKeyframe(keyframes, time);
}
case 'after': {
return findNextKeyframe(keyframes, time);
}
default: {
return undefined;
}
}
}
@ -125,7 +138,7 @@ export function getSafeCutTime(frames, cutTime, nextMode) {
if (frames.length < 2) throw new Error(i18n.t('Less than 2 frames found'));
if (nextMode) {
index = frames.findIndex(f => f.keyframe && f.time >= cutTime - sigma);
index = frames.findIndex((f) => f.keyframe && f.time >= cutTime - sigma);
if (index === -1) throw new Error(i18n.t('Failed to find next keyframe'));
if (index >= frames.length - 1) throw new Error(i18n.t('We are on the last frame'));
const { time } = frames[index];
@ -136,12 +149,13 @@ export function getSafeCutTime(frames, cutTime, nextMode) {
}
const findReverseIndex = (arr, cb) => {
// eslint-disable-next-line unicorn/no-array-callback-reference
const ret = [...arr].reverse().findIndex(cb);
if (ret === -1) return -1;
return arr.length - 1 - ret;
};
index = findReverseIndex(frames, f => f.time <= cutTime + sigma);
index = findReverseIndex(frames, (f) => f.time <= cutTime + sigma);
if (index === -1) throw new Error(i18n.t('Failed to find any prev frame'));
if (index === 0) throw new Error(i18n.t('We are on the first frame'));
@ -155,7 +169,7 @@ export function getSafeCutTime(frames, cutTime, nextMode) {
}
// We are not on a frame before keyframe, look for preceding keyframe instead
index = findReverseIndex(frames, f => f.keyframe && f.time <= cutTime + sigma);
index = findReverseIndex(frames, (f) => f.keyframe && f.time <= cutTime + sigma);
if (index === -1) throw new Error(i18n.t('Failed to find any prev keyframe'));
if (index === 0) throw new Error(i18n.t('We are on the first keyframe'));
@ -165,9 +179,9 @@ export function getSafeCutTime(frames, cutTime, nextMode) {
export function findNearestKeyFrameTime({ frames, time, direction, fps }) {
const sigma = fps ? (1 / fps) : 0.1;
const keyframes = frames.filter(f => f.keyframe && (direction > 0 ? f.time > time + sigma : f.time < time - sigma));
const keyframes = frames.filter((f) => f.keyframe && (direction > 0 ? f.time > time + sigma : f.time < time - sigma));
if (keyframes.length === 0) return undefined;
const nearestKeyFrame = sortBy(keyframes, keyframe => (direction > 0 ? keyframe.time - time : time - keyframe.time))[0];
const nearestKeyFrame = sortBy(keyframes, (keyframe) => (direction > 0 ? keyframe.time - time : time - keyframe.time))[0];
if (!nearestKeyFrame) return undefined;
return nearestKeyFrame.time;
}
@ -186,7 +200,7 @@ export async function tryMapChaptersToEdl(chapters) {
end,
name,
};
}).filter((it) => it);
}).filter(Boolean);
} catch (err) {
console.error('Failed to read chapters from file', err);
return [];
@ -218,6 +232,7 @@ export async function createChaptersFromSegments({ segmentPaths, chapterNames })
function mapDefaultFormat({ streams, requestedFormat }) {
if (requestedFormat === 'mp4') {
// Only MOV supports these codecs, so default to MOV instead https://github.com/mifi/lossless-cut/issues/948
// eslint-disable-next-line unicorn/no-lonely-if
if (streams.some((stream) => pcmAudioCodecs.includes(stream.codec_name))) {
return 'mov';
}
@ -230,7 +245,7 @@ function mapDefaultFormat({ streams, requestedFormat }) {
}
async function determineOutputFormat(ffprobeFormatsStr, filePath) {
const ffprobeFormats = (ffprobeFormatsStr || '').split(',').map((str) => str.trim()).filter((str) => str);
const ffprobeFormats = (ffprobeFormatsStr || '').split(',').map((str) => str.trim()).filter(Boolean);
if (ffprobeFormats.length === 0) {
console.warn('ffprobe returned unknown formats', ffprobeFormatsStr);
return undefined;
@ -256,19 +271,30 @@ async function determineOutputFormat(ffprobeFormatsStr, filePath) {
// https://www.ftyps.com/
// https://exiftool.org/TagNames/QuickTime.html
switch (fileTypeResponse.mime) {
case 'video/x-matroska': return 'matroska';
case 'video/webm': return 'webm';
case 'video/quicktime': return 'mov';
case 'video/3gpp2': return '3g2';
case 'video/3gpp': return '3gp';
case 'video/x-matroska': {
return 'matroska';
}
case 'video/webm': {
return 'webm';
}
case 'video/quicktime': {
return 'mov';
}
case 'video/3gpp2': {
return '3g2';
}
case 'video/3gpp': {
return '3gp';
}
// These two cmds produce identical output, so we assume that encoding "ipod" means encoding m4a
// ffmpeg -i example.aac -c copy OutputFile2.m4a
// ffmpeg -i example.aac -c copy -f ipod OutputFile.m4a
// See also https://github.com/mifi/lossless-cut/issues/28
case 'audio/x-m4a':
case 'audio/mp4':
case 'audio/mp4': {
return 'ipod';
}
case 'image/avif':
case 'image/heif':
case 'image/heif-sequence':
@ -276,8 +302,9 @@ async function determineOutputFormat(ffprobeFormatsStr, filePath) {
case 'image/heic-sequence':
case 'video/x-m4v':
case 'video/mp4':
case 'image/x-canon-cr3':
case 'image/x-canon-cr3': {
return 'mp4';
}
default: {
console.warn('file-type returned unknown format', ffprobeFormats, fileTypeResponse.mime);
@ -303,7 +330,7 @@ export async function readFileMeta(filePath) {
try {
// https://github.com/mifi/lossless-cut/issues/1342
parsedJson = JSON.parse(stdout);
} catch (err) {
} catch {
console.log('ffprobe stdout', stdout);
throw new Error('ffprobe returned malformed data');
}
@ -482,16 +509,16 @@ export async function extractSubtitleTrack(filePath, streamId) {
export async function renderThumbnails({ filePath, from, duration, onThumbnail }) {
// Time first render to determine how many to render
const startTime = new Date().getTime() / 1000;
const startTime = Date.now() / 1000;
let url = await renderThumbnail(filePath, from);
const endTime = new Date().getTime() / 1000;
const endTime = Date.now() / 1000;
onThumbnail({ time: from, url });
// Aim for max 3 sec to render all
const numThumbs = Math.floor(Math.min(Math.max(3 / (endTime - startTime), 3), 10));
// console.log(numThumbs);
const thumbTimes = Array(numThumbs - 1).fill(undefined).map((_unused, i) => (from + ((duration * (i + 1)) / (numThumbs))));
const thumbTimes = Array.from({ length: numThumbs - 1 }).fill(undefined).map((_unused, i) => (from + ((duration * (i + 1)) / (numThumbs))));
// console.log(thumbTimes);
await pMap(thumbTimes, async (time) => {
@ -504,7 +531,7 @@ export async function extractWaveform({ filePath, outPath }) {
const numSegs = 10;
const duration = 60 * 60;
const maxLen = 0.1;
const segments = Array(numSegs).fill(undefined).map((_unused, i) => [i * (duration / numSegs), Math.min(duration / numSegs, maxLen)] as const);
const segments = Array.from({ length: numSegs }).fill(undefined).map((_unused, i) => [i * (duration / numSegs), Math.min(duration / numSegs, maxLen)] as const);
// https://superuser.com/questions/681885/how-can-i-remove-multiple-segments-from-a-video-using-ffmpeg
let filter = segments.map(([from, len], i) => `[0:a]atrim=start=${from}:end=${from + len},asetpts=PTS-STARTPTS[a${i}]`).join(';');
@ -541,7 +568,7 @@ export function isProblematicAvc1(outFormat, streams) {
}
function parseFfprobeFps(stream) {
const match = typeof stream.avg_frame_rate === 'string' && stream.avg_frame_rate.match(/^([0-9]+)\/([0-9]+)$/);
const match = typeof stream.avg_frame_rate === 'string' && stream.avg_frame_rate.match(/^(\d+)\/(\d+)$/);
if (!match) return undefined;
const num = parseInt(match[1], 10);
const den = parseInt(match[2], 10);
@ -555,6 +582,7 @@ export function getStreamFps(stream) {
return fps;
}
if (stream.codec_type === 'audio') {
// eslint-disable-next-line unicorn/no-lonely-if
if (typeof stream.sample_rate === 'string') {
const sampleRate = parseInt(stream.sample_rate, 10);
if (!Number.isNaN(sampleRate) && sampleRate > 0) {
@ -595,7 +623,7 @@ export function getTimecodeFromStreams(streams) {
return true;
}
return undefined;
} catch (err) {
} catch {
// console.warn('Failed to parse timecode from file streams', err);
return undefined;
}

View File

@ -1,5 +1,4 @@
// Taken from: https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js
/* eslint-disable */
/**
* Copyright (c) 2015, Facebook, Inc.
@ -13,12 +12,10 @@
* @typechecks
*/
'use strict';
// Reasonable defaults
var PIXEL_STEP = 10;
var LINE_HEIGHT = 40;
var PAGE_HEIGHT = 800;
const PIXEL_STEP = 10;
const LINE_HEIGHT = 40;
const PAGE_HEIGHT = 800;
/**
* Mouse wheel (and 2-finger trackpad) support on the web sucks. It is
@ -120,9 +117,9 @@ var PAGE_HEIGHT = 800;
* Firefox v4/Win7 | undefined | 3
*
*/
export default function normalizeWheel(/*object*/ event) /*object*/ {
var sX = 0, sY = 0, // spinX, spinY
pX = 0, pY = 0; // pixelX, pixelY
export default function normalizeWheel(/* object */ event) /* object */ {
let sX = 0; let sY = 0; // spinX, spinY
let pX = 0; let pY = 0; // pixelX, pixelY
// Legacy
if ('detail' in event) { sY = event.detail; }
@ -131,7 +128,7 @@ export default function normalizeWheel(/*object*/ event) /*object*/ {
if ('wheelDeltaX' in event) { sX = -event.wheelDeltaX / 120; }
// side scrolling on FF with DOMMouseScroll
if ( 'axis' in event && event.axis === event.HORIZONTAL_AXIS ) {
if ('axis' in event && event.axis === event.HORIZONTAL_AXIS) {
sX = sY;
sY = 0;
}
@ -143,7 +140,7 @@ export default function normalizeWheel(/*object*/ event) /*object*/ {
if ('deltaX' in event) { pX = event.deltaX; }
if ((pX || pY) && event.deltaMode) {
if (event.deltaMode == 1) { // delta in LINE units
if (event.deltaMode === 1) { // delta in LINE units
pX *= LINE_HEIGHT;
pY *= LINE_HEIGHT;
} else { // delta in PAGE units
@ -156,8 +153,10 @@ export default function normalizeWheel(/*object*/ event) /*object*/ {
if (pX && !sX) { sX = (pX < 1) ? -1 : 1; }
if (pY && !sY) { sY = (pY < 1) ? -1 : 1; }
return { spinX : sX,
spinY : sY,
pixelX : pX,
pixelY : pY };
return {
spinX: sX,
spinY: sY,
pixelX: pX,
pixelY: pY,
};
}

View File

@ -7,7 +7,12 @@ import { errorToast } from '../swal';
// eslint-disable-next-line no-unused-vars
import isDev from '../isDev';
export class DirectoryAccessDeclinedError extends Error {}
export class DirectoryAccessDeclinedError extends Error {
constructor() {
super();
this.name = 'DirectoryAccessDeclinedError';
}
}
// MacOS App Store sandbox doesn't allow reading/writing anywhere,
// except those exact file paths that have been explicitly drag-dropped into LosslessCut or opened using the opener dialog

View File

@ -16,7 +16,7 @@ const { writeFile, mkdir } = window.require('fs/promises');
async function writeChaptersFfmetadata(outDir, chapters) {
if (!chapters || chapters.length === 0) return undefined;
const path = join(outDir, `ffmetadata-${new Date().getTime()}.txt`);
const path = join(outDir, `ffmetadata-${Date.now()}.txt`);
const ffmetadata = chapters.map(({ start, end, name }) => (
`[CHAPTER]\nTIMEBASE=1/1000\nSTART=${Math.floor(start * 1000)}\nEND=${Math.floor(end * 1000)}\ntitle=${name || ''}`
@ -37,7 +37,7 @@ function getMovFlags({ preserveMovData, movFastStart }) {
if (movFastStart) flags.push('+faststart');
if (flags.length === 0) return [];
return flatMap(flags, flag => ['-movflags', flag]);
return flatMap(flags, (flag) => ['-movflags', flag]);
}
function getMatroskaFlags() {
@ -153,7 +153,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
// https://superuser.com/questions/787064/filename-quoting-in-ffmpeg-concat
// Must add "file:" or we get "Impossible to open 'pipe:xyz.mp4'" on newer ffmpeg versions
// https://superuser.com/questions/718027/ffmpeg-concat-doesnt-work-with-absolute-path
const concatTxt = paths.map(file => `file 'file:${resolve(file).replace(/'/g, "'\\''")}'`).join('\n');
const concatTxt = paths.map((file) => `file 'file:${resolve(file).replaceAll('\'', "'\\''")}'`).join('\n');
const ffmpegCommandLine = getFfCommandLine('ffmpeg', ffmpegArgs);

View File

@ -4,7 +4,7 @@ import { useEffect, useRef } from 'react';
// Also document.addEventListener needs custom handling of modifier keys or C will be triggered by CTRL+C, etc
import Mousetrap from 'mousetrap';
const keyupActions = ['seekBackwards', 'seekForwards'];
const keyupActions = new Set(['seekBackwards', 'seekForwards']);
export default ({ keyBindings, onKeyPress: onKeyPressProp }) => {
const onKeyPressRef = useRef();
@ -25,7 +25,7 @@ export default ({ keyBindings, onKeyPress: onKeyPressProp }) => {
keyBindings.forEach(({ action, keys }) => {
mousetrap.bind(keys, () => onKeyPress({ action }));
if (keyupActions.includes(action)) {
if (keyupActions.has(action)) {
mousetrap.bind(keys, () => onKeyPress({ action, keyup: true }), 'keyup');
}
});

View File

@ -117,7 +117,7 @@ export default ({
const currentCutSeg = useMemo(() => cutSegments[currentSegIndexSafe], [currentSegIndexSafe, cutSegments]);
const currentApparentCutSeg = useMemo(() => apparentCutSegments[currentSegIndexSafe], [apparentCutSegments, currentSegIndexSafe]);
const selectedSegmentsRaw = useMemo(() => apparentCutSegments.filter(isSegmentSelected), [apparentCutSegments, isSegmentSelected]);
const selectedSegmentsRaw = useMemo(() => apparentCutSegments.filter((segment) => isSegmentSelected(segment)), [apparentCutSegments, isSegmentSelected]);
const detectBlackScenes = useCallback(async () => {
const filterOptions = await showParametersDialog({ title: i18n.t('Enter parameters'), parameters: ffmpegParameters.blackdetect(), docUrl: 'https://ffmpeg.org/ffmpeg-filters.html#blackdetect' });
@ -170,7 +170,7 @@ export default ({
}, [apparentCutSegments, duration, haveInvalidSegs]);
const invertAllSegments = useCallback(() => {
if (inverseCutSegments.length < 1) {
if (inverseCutSegments.length === 0) {
errorToast(i18n.t('Make sure you have no overlapping segments.'));
return;
}
@ -180,7 +180,7 @@ export default ({
}, [inverseCutSegments, setCutSegments]);
const fillSegmentsGaps = useCallback(() => {
if (inverseCutSegments.length < 1) {
if (inverseCutSegments.length === 0) {
errorToast(i18n.t('Make sure you have no overlapping segments.'));
return;
}
@ -227,7 +227,7 @@ export default ({
return newSegment;
}, { concurrency });
newSegments = newSegments.filter((segment) => segment.end > segment.start);
if (newSegments.length < 1) setCutSegments(createInitialCutSegments());
if (newSegments.length === 0) setCutSegments(createInitialCutSegments());
else setCutSegments(newSegments);
}, [apparentCutSegments, createInitialCutSegments, duration, isSegmentSelected, setCutSegments]);
@ -449,7 +449,7 @@ export default ({
}, [cutSegments, enableSegments]);
const onLabelSelectedSegments = useCallback(async () => {
if (selectedSegmentsRaw.length < 1) return;
if (selectedSegmentsRaw.length === 0) return;
const { name } = selectedSegmentsRaw[0];
const value = await labelSegmentDialog({ currentName: name, maxLength: maxLabelLength });
if (value == null) return;

View File

@ -42,4 +42,5 @@ i18n
},
});
// eslint-disable-next-line unicorn/prefer-export-from
export default i18n;

View File

@ -84,7 +84,7 @@ it('detects overlapping segments', () => {
]);
expect(partitionIntoOverlappingRanges([
{ start: 9, end: 10.50 },
{ start: 9, end: 10.5 },
{ start: 11, end: 12 },
{ start: 11.5, end: 12.5 },
{ start: 11.5, end: 13 },

View File

@ -99,11 +99,11 @@ export function combineOverlappingSegments(existingSegments, getSegApparentEnd2)
};
}
return undefined; // then remove all other segments in this partition group
}).filter((segment) => segment);
}).filter(Boolean);
}
export function combineSelectedSegments<T extends SegmentBase>(existingSegments: T[], getSegApparentEnd2, isSegmentSelected) {
const selectedSegments = existingSegments.filter(isSegmentSelected);
const selectedSegments = existingSegments.filter((segment) => isSegmentSelected(segment));
const firstSegment = minBy(selectedSegments, (seg) => getSegApparentStart(seg));
const lastSegment = maxBy(selectedSegments, (seg) => getSegApparentEnd2(seg));
@ -117,18 +117,18 @@ export function combineSelectedSegments<T extends SegmentBase>(existingSegments:
}
if (isSegmentSelected(existingSegment)) return undefined; // remove other selected segments
return existingSegment;
}).filter((segment) => segment);
}).filter(Boolean);
}
export function hasAnySegmentOverlap(sortedSegments) {
if (sortedSegments.length < 1) return false;
if (sortedSegments.length === 0) return false;
const overlappingGroups = partitionIntoOverlappingRanges(sortedSegments);
return overlappingGroups.length > 0;
}
export function invertSegments(sortedCutSegments, includeFirstSegment: boolean, includeLastSegment: boolean, duration?: number) {
if (sortedCutSegments.length < 1) return undefined;
if (sortedCutSegments.length === 0) return undefined;
if (hasAnySegmentOverlap(sortedCutSegments)) return undefined;
@ -157,7 +157,7 @@ export function invertSegments(sortedCutSegments, includeFirstSegment: boolean,
});
if (includeLastSegment) {
const lastSeg = sortedCutSegments[sortedCutSegments.length - 1];
const lastSeg = sortedCutSegments.at(-1);
if (duration == null || lastSeg.end < duration) {
const inverted: InverseSegment = {
start: lastSeg.end,
@ -175,7 +175,7 @@ export function invertSegments(sortedCutSegments, includeFirstSegment: boolean,
// because chapters need to be contiguous, we need to insert gaps in-between
export function convertSegmentsToChapters(sortedSegments) {
if (sortedSegments.length < 1) return [];
if (sortedSegments.length === 0) return [];
if (hasAnySegmentOverlap(sortedSegments)) throw new Error('Segments cannot overlap');
sortedSegments.map((segment) => ({ start: segment.start, end: segment.end, name: segment.name }));

View File

@ -163,12 +163,12 @@ export function handleError(arg1: unknown, arg2?: unknown) {
toast.fire({
icon: 'error',
title: msg || i18n.t('An error has occurred.'),
text: errorMsg ? errorMsg.substring(0, 300) : undefined,
text: errorMsg ? errorMsg.slice(0, 300) : undefined,
});
}
export function filenamify(name) {
return name.replace(/[^0-9a-zA-Z_\-.]/g, '_');
return name.replaceAll(/[^\w.-]/g, '_');
}
export function withBlur(cb) {
@ -248,7 +248,7 @@ export async function findExistingHtml5FriendlyFile(fp, cod) {
matches = [...matches, ...nonMatches];
// console.log(matches);
if (matches.length < 1) return undefined;
if (matches.length === 0) return undefined;
const { suffix, entry } = matches[0]!;
@ -318,7 +318,7 @@ export async function checkAppPath() {
// eslint-disable-next-line no-useless-concat, one-var, one-var-declaration-per-line
const mf = 'mi' + 'fi.no', llc = 'Los' + 'slessC' + 'ut';
const appPath = isDev ? 'C:\\Program Files\\WindowsApps\\37672NoveltyStudio.MediaConverter_9.0.6.0_x64__vjhnv588cyf84' : remote.app.getAppPath();
const pathMatch = appPath.replace(/\\/g, '/').match(/Windows ?Apps\/([^/]+)/); // find the first component after WindowsApps
const pathMatch = appPath.replaceAll('\\', '/').match(/Windows ?Apps\/([^/]+)/); // find the first component after WindowsApps
// example pathMatch: 37672NoveltyStudio.MediaConverter_9.0.6.0_x64__vjhnv588cyf84
if (!pathMatch) {
console.warn('Unknown path match', appPath);
@ -358,7 +358,8 @@ export function shuffleArray(arrayIn) {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
export function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
// eslint-disable-next-line unicorn/better-regex
return string.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
export const readFileSize = async (path) => (await stat(path)).size;
@ -377,8 +378,7 @@ export function checkFileSizes(inputSize, outputSize) {
function setDocumentExtraTitle(extra) {
const baseTitle = 'LosslessCut';
if (extra != null) document.title = `${baseTitle} - ${extra}`;
else document.title = baseTitle;
document.title = extra != null ? `${baseTitle} - ${extra}` : baseTitle;
}
export function setDocumentTitle({ filePath, working, cutProgress }: { filePath: string, working?: string, cutProgress?: number }) {
@ -402,7 +402,7 @@ export function mustDisallowVob() {
export async function readVideoTs(videoTsPath) {
const files = await readdir(videoTsPath);
const relevantFiles = files.filter((file) => /^VTS_\d+_\d+\.vob$/i.test(file) && !/^VTS_\d+_00\.vob$/i.test(file)); // skip menu
const relevantFiles = files.filter((file) => /^vts_\d+_\d+\.vob$/i.test(file) && !/^vts_\d+_00\.vob$/i.test(file)); // skip menu
const ret = sortBy(relevantFiles).map((file) => join(videoTsPath, file));
if (ret.length === 0) throw new Error('No VTS vob files found in folder');
return ret;

View File

@ -43,7 +43,8 @@ export const isExactDurationMatch = (str) => /^-?\d{2}:\d{2}:\d{2}.\d{3}$/.test(
// See also parseYoutube
export function parseDuration(str) {
const match = str.replace(/\s/g, '').match(/^(-?)(?:(?:(\d{1,}):)?(\d{1,2}):)?(\d{1,2}(?:[.,]\d{1,3})?)$/);
// eslint-disable-next-line unicorn/better-regex
const match = str.replaceAll(/\s/g, '').match(/^(-?)(?:(?:(\d{1,}):)?(\d{1,2}):)?(\d{1,2}(?:[.,]\d{1,3})?)$/);
if (!match) return undefined;

View File

@ -126,7 +126,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).substr(0, maxLabelLength);
const filenamifyOrNot = (fileName) => (safeOutputFileName || forceSafeOutputFileName ? filenamify(fileName) : fileName).slice(0, Math.max(0, maxLabelLength));
function getSegSuffix() {
if (name) return `-${filenamifyOrNot(name)}`;
@ -159,7 +159,7 @@ export function generateOutSegFileNames({ segments, template: desiredTemplate, f
return [
...rest,
// If sanitation is enabled, make sure filename (last seg of the path) is not too long
safeOutputFileName ? lastSeg!.substring(0, 200) : lastSeg,
safeOutputFileName ? lastSeg!.slice(0, 200) : lastSeg,
].join(pathSep);
});
}

View File

@ -1,3 +1,4 @@
// eslint-disable-line unicorn/filename-case
import clamp from 'lodash/clamp';
/**
@ -24,12 +25,12 @@ export function adjustRate(playbackRate, direction, multiplier) {
// stop along the way at 1.0. This could happen if the current playbackRate was reached
// using a different multiplier (e.g., holding the shift key).
// https://github.com/mifi/lossless-cut/issues/447#issuecomment-766339083
if ((newRate > 1.0 && playbackRate < 1.0) || (newRate < 1.0 && playbackRate > 1.0)) {
newRate = 1.0;
if ((newRate > 1 && playbackRate < 1) || (newRate < 1 && playbackRate > 1)) {
newRate = 1;
}
// And, clean up any rounding errors that get us to almost 1.0 (e.g., treat 1.00001 as 1)
if ((newRate > (m ** (-1 / 2))) && (newRate < (m ** (1 / 2)))) {
newRate = 1.0;
newRate = 1;
}
return clamp(newRate, 0.1, 16);
}

View File

@ -1,3 +1,4 @@
// eslint-disable-line unicorn/filename-case
import { describe, it, expect } from 'vitest';
import { adjustRate, DEFAULT_PLAYBACK_RATE } from './rate-calculator';

View File

@ -1,14 +1,14 @@
// https://www.ffmpeg.org/doxygen/3.2/libavutil_2utils_8c_source.html#l00079
const defaultProcessedCodecTypes = [
const defaultProcessedCodecTypes = new Set([
'video',
'audio',
'subtitle',
'attachment',
];
]);
const unprocessableCodecs = [
const unprocessableCodecs = new Set([
'dvb_teletext', // ffmpeg doesn't seem to support this https://github.com/mifi/lossless-cut/issues/1343
];
]);
// taken from `ffmpeg -codecs`
export const pcmAudioCodecs = [
@ -122,6 +122,7 @@ function getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposi
addArgs(`-c:${outputIndex}`, codec);
}
// eslint-disable-next-line unicorn/prefer-switch
if (stream.codec_type === 'subtitle') {
// mp4/mov only supports mov_text, so convert it https://stackoverflow.com/a/17584272/6519037
// https://github.com/mifi/lossless-cut/issues/418
@ -135,6 +136,7 @@ function getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposi
} else if (outFormat === 'webm' && stream.codec_name === 'mov_text') {
// Only WebVTT subtitles are supported for WebM.
addCodecArgs('webvtt');
// eslint-disable-next-line unicorn/prefer-switch
} else if (outFormat === 'srt') { // not technically lossless but why not
addCodecArgs('srt');
} else if (outFormat === 'ass') { // not technically lossless but why not
@ -171,6 +173,7 @@ function getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposi
if (isMov(outFormat)) {
// 0x31766568 see https://github.com/mifi/lossless-cut/issues/1444
// eslint-disable-next-line unicorn/prefer-switch, unicorn/no-lonely-if
if (['0x0000', '0x31637668', '0x31766568'].includes(stream.codec_tag) && stream.codec_name === 'hevc') {
addArgs(`-tag:${outputIndex}`, 'hvc1');
}
@ -214,8 +217,8 @@ export function getMapStreamsArgs({ startIndex = 0, outFormat, allFilesMeta, cop
}
export function shouldCopyStreamByDefault(stream) {
if (!defaultProcessedCodecTypes.includes(stream.codec_type)) return false;
if (unprocessableCodecs.includes(stream.codec_name)) return false;
if (!defaultProcessedCodecTypes.has(stream.codec_type)) return false;
if (unprocessableCodecs.has(stream.codec_name)) return false;
return true;
}
@ -225,9 +228,9 @@ export function isStreamThumbnail(stream) {
return stream && stream.codec_type === 'video' && stream.disposition?.[attachedPicDisposition] === 1;
}
export const getAudioStreams = (streams) => streams.filter(stream => stream.codec_type === 'audio');
export const getRealVideoStreams = (streams) => streams.filter(stream => stream.codec_type === 'video' && !isStreamThumbnail(stream));
export const getSubtitleStreams = (streams) => streams.filter(stream => stream.codec_type === 'subtitle');
export const getAudioStreams = (streams) => streams.filter((stream) => stream.codec_type === 'audio');
export const getRealVideoStreams = (streams) => streams.filter((stream) => stream.codec_type === 'video' && !isStreamThumbnail(stream));
export const getSubtitleStreams = (streams) => streams.filter((stream) => stream.codec_type === 'subtitle');
// videoTracks/audioTracks seems to be 1-indexed, while ffmpeg is 0-indexes
const getHtml5TrackId = (ffmpegTrackIndex: number) => String(ffmpegTrackIndex + 1);

1973
yarn.lock

File diff suppressed because it is too large Load Diff