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:
parent
e7c85dd8b5
commit
4704d246b4
@ -1,47 +1,37 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: [
|
extends: ['mifi'],
|
||||||
'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',
|
|
||||||
],
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
browser: true,
|
|
||||||
},
|
|
||||||
rules: {
|
rules: {
|
||||||
'max-len': 0,
|
|
||||||
'import/no-extraneous-dependencies': ['error', {
|
|
||||||
devDependencies: true,
|
|
||||||
optionalDependencies: false,
|
|
||||||
}],
|
|
||||||
'no-console': 0,
|
'no-console': 0,
|
||||||
'jsx-a11y/click-events-have-key-events': 0,
|
'jsx-a11y/click-events-have-key-events': 0,
|
||||||
'jsx-a11y/interactive-supports-focus': 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,
|
'jsx-a11y/control-has-associated-label': 0,
|
||||||
'react/prop-types': 0,
|
'unicorn/prefer-node-protocol': 0, // todo
|
||||||
'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
|
'@typescript-eslint/no-var-requires': 0, // todo
|
||||||
'react/display-name': 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,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['./src/**/*.{js,cjs,mjs,jsx,ts,tsx,mts}'],
|
||||||
|
env: {
|
||||||
|
node: false,
|
||||||
|
browser: true,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'import/no-extraneous-dependencies': ['error', {
|
||||||
|
devDependencies: true,
|
||||||
|
optionalDependencies: false,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['./script/**/*.{js,cjs,mjs,jsx,ts,tsx,mts}', 'vite.config.js'],
|
||||||
|
rules: {
|
||||||
|
'import/no-extraneous-dependencies': ['error', {
|
||||||
|
devDependencies: true,
|
||||||
|
optionalDependencies: false,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
@ -21,10 +21,10 @@ Note: `yarn` may take some time to complete.
|
|||||||
|
|
||||||
Run one of the below commands:
|
Run one of the below commands:
|
||||||
```bash
|
```bash
|
||||||
npm run download-ffmpeg-darwin-x64
|
yarn download-ffmpeg-darwin-x64
|
||||||
npm run download-ffmpeg-darwin-arm64
|
yarn download-ffmpeg-darwin-arm64
|
||||||
npm run download-ffmpeg-linux-x64
|
yarn download-ffmpeg-linux-x64
|
||||||
npm run download-ffmpeg-win32-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`.
|
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
|
### Running
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## `mas-dev` (Mac App Store) local build
|
## `mas-dev` (Mac App Store) local build
|
||||||
@ -40,7 +40,7 @@ npm start
|
|||||||
This will sign using the development provisioning profile:
|
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.
|
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
|
- If Mac App Store / Windows Store
|
||||||
- Merge `stores` into `master`
|
- Merge `stores` into `master`
|
||||||
- Bump [snap version](https://snapcraft.io/losslesscut/listing)
|
- 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
|
## Minimum OS version
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ Minimum supported OS versions for Electron. As of electron 22:
|
|||||||
How to check the value:
|
How to check the value:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run pack-mas-dev
|
yarn pack-mas-dev
|
||||||
cat dist/mas-dev-arm64/LosslessCut.app/Contents/Info.plist
|
cat dist/mas-dev-arm64/LosslessCut.app/Contents/Info.plist
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ Links:
|
|||||||
- package.json
|
- package.json
|
||||||
|
|
||||||
### i18n
|
### i18n
|
||||||
`npm run scan-i18n`
|
`yarn scan-i18n`
|
||||||
|
|
||||||
### Licenses
|
### Licenses
|
||||||
|
|
||||||
@ -139,7 +139,7 @@ npx license-checker --summary
|
|||||||
#### Regenerate licenses file
|
#### Regenerate licenses file
|
||||||
|
|
||||||
```
|
```
|
||||||
npm run generate-licenses
|
yarn generate-licenses
|
||||||
#cp licenses.txt losslesscut.mifi.no/public/
|
#cp licenses.txt losslesscut.mifi.no/public/
|
||||||
```
|
```
|
||||||
Then deploy.
|
Then deploy.
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// eslint-disable-line unicorn/filename-case
|
||||||
export default {
|
export default {
|
||||||
input: ['src/**/*.{js,jsx,ts,tsx}', 'public/*.{js,ts}'],
|
input: ['src/**/*.{js,jsx,ts,tsx}', 'public/*.{js,ts}'],
|
||||||
|
|
||||||
|
24
package.json
24
package.json
@ -7,9 +7,9 @@
|
|||||||
"main": "public/electron.js",
|
"main": "public/electron.js",
|
||||||
"homepage": "./",
|
"homepage": "./",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "concurrently -k \"npm run start:frontend\" \"npm run start:electron\"",
|
"dev": "concurrently -k \"npm run dev:frontend\" \"npm run dev:electron\"",
|
||||||
"start:frontend": "cross-env vite --port 3001",
|
"dev:frontend": "cross-env vite --port 3001",
|
||||||
"start:electron": "wait-on tcp:3001 && electron .",
|
"dev:electron": "wait-on tcp:3001 && electron .",
|
||||||
"icon-gen": "mkdirp icon-build build-resources/appx && node script/icon-gen.mjs",
|
"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-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",
|
"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",
|
"build": "yarn icon-gen && vite build --outDir vite-dist",
|
||||||
"tsc": "tsc --build",
|
"tsc": "tsc --build",
|
||||||
"test": "vitest",
|
"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",
|
"pack-mac": "electron-builder --mac -m dmg",
|
||||||
"prepack-mac": "yarn build",
|
"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)'",
|
"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",
|
"@radix-ui/react-switch": "^1.0.1",
|
||||||
"@tsconfig/strictest": "^2.0.2",
|
"@tsconfig/strictest": "^2.0.2",
|
||||||
"@tsconfig/vite-react": "^3.0.0",
|
"@tsconfig/vite-react": "^3.0.0",
|
||||||
|
"@types/eslint": "^8",
|
||||||
"@types/lodash": "^4.14.202",
|
"@types/lodash": "^4.14.202",
|
||||||
"@types/sortablejs": "^1.15.0",
|
"@types/sortablejs": "^1.15.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||||
"@typescript-eslint/parser": "^6.17.0",
|
"@typescript-eslint/parser": "^6.12.0",
|
||||||
"@uidotdev/usehooks": "^2.4.1",
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
"@vitejs/plugin-react": "^3.1.0",
|
"@vitejs/plugin-react": "^3.1.0",
|
||||||
"color": "^3.1.0",
|
"color": "^3.1.0",
|
||||||
@ -61,13 +62,13 @@
|
|||||||
"electron": "^27.0.0",
|
"electron": "^27.0.0",
|
||||||
"electron-builder": "^24.6.4",
|
"electron-builder": "^24.6.4",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"eslint": "^8.53.0",
|
"eslint": "^8.2.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-mifi": "^0.0.3",
|
||||||
"eslint-import-resolver-typescript": "^3.6.1",
|
|
||||||
"eslint-plugin-import": "^2.25.3",
|
"eslint-plugin-import": "^2.25.3",
|
||||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||||
"eslint-plugin-react": "^7.28.0",
|
"eslint-plugin-react": "^7.28.0",
|
||||||
"eslint-plugin-react-hooks": "^4.3.0",
|
"eslint-plugin-react-hooks": "^4.3.0",
|
||||||
|
"eslint-plugin-unicorn": "^51.0.1",
|
||||||
"evergreen-ui": "^6.13.1",
|
"evergreen-ui": "^6.13.1",
|
||||||
"fast-xml-parser": "^4.2.5",
|
"fast-xml-parser": "^4.2.5",
|
||||||
"framer-motion": "^9.0.3",
|
"framer-motion": "^9.0.3",
|
||||||
@ -97,7 +98,7 @@
|
|||||||
"sortablejs": "^1.13.0",
|
"sortablejs": "^1.13.0",
|
||||||
"sweetalert2": "^11.0.0",
|
"sweetalert2": "^11.0.0",
|
||||||
"sweetalert2-react-content": "^5.0.7",
|
"sweetalert2-react-content": "^5.0.7",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "~5.2.0",
|
||||||
"typescript-plugin-css-modules": "^5.1.0",
|
"typescript-plugin-css-modules": "^5.1.0",
|
||||||
"use-debounce": "^5.1.0",
|
"use-debounce": "^5.1.0",
|
||||||
"use-trace-update": "^1.3.0",
|
"use-trace-update": "^1.3.0",
|
||||||
@ -132,9 +133,6 @@
|
|||||||
"winston": "^3.8.1",
|
"winston": "^3.8.1",
|
||||||
"yargs-parser": "^21.0.0"
|
"yargs-parser": "^21.0.0"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
|
||||||
"extends": "react-app"
|
|
||||||
},
|
|
||||||
"build": {
|
"build": {
|
||||||
"directories": {
|
"directories": {
|
||||||
"buildResources": "build-resources"
|
"buildResources": "build-resources"
|
||||||
|
@ -7,6 +7,7 @@ function createMediaSourceStream({ path, videoStreamIndex, audioStreamIndex, see
|
|||||||
logger.info('Starting preview process', { videoStreamIndex, audioStreamIndex, seekTo });
|
logger.info('Starting preview process', { videoStreamIndex, audioStreamIndex, seekTo });
|
||||||
const process = createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, seekTo, size, fps });
|
const process = createMediaSourceProcess({ path, videoStreamIndex, audioStreamIndex, seekTo, size, fps });
|
||||||
|
|
||||||
|
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
||||||
abortController.signal.onabort = () => {
|
abortController.signal.onabort = () => {
|
||||||
logger.info('Aborting preview process', { videoStreamIndex, audioStreamIndex, seekTo });
|
logger.info('Aborting preview process', { videoStreamIndex, audioStreamIndex, seekTo });
|
||||||
process.kill('SIGKILL');
|
process.kill('SIGKILL');
|
||||||
@ -64,7 +65,7 @@ function createMediaSourceStream({ path, videoStreamIndex, audioStreamIndex, see
|
|||||||
|
|
||||||
if (!err.killed) {
|
if (!err.killed) {
|
||||||
console.warn(err.message);
|
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 abortController = new AbortController();
|
||||||
const process = readOneJpegFrame({ path, seekTo, videoStreamIndex });
|
const process = readOneJpegFrame({ path, seekTo, videoStreamIndex });
|
||||||
|
|
||||||
|
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
||||||
abortController.signal.onabort = () => process.kill('SIGKILL');
|
abortController.signal.onabort = () => process.kill('SIGKILL');
|
||||||
|
|
||||||
function abort() {
|
function abort() {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
const Store = require('electron-store');
|
const Store = require('electron-store');
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
const electron = require('electron');
|
const electron = require('electron');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const { join, dirname } = require('path');
|
const { join, dirname } = require('path');
|
||||||
@ -175,7 +176,7 @@ async function tryCreateStore({ customStoragePath }) {
|
|||||||
return;
|
return;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// 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);
|
logger.error('Failed to create config store, retrying', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -198,11 +199,9 @@ async function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cleanupChoices = store.get('cleanupChoices'); // todo remove after a while
|
const cleanupChoices = store.get('cleanupChoices'); // todo remove after a while
|
||||||
if (cleanupChoices != null) {
|
if (cleanupChoices != null && cleanupChoices.closeFile == null) {
|
||||||
if (cleanupChoices.closeFile == null) {
|
logger.info('Migrating cleanupChoices.closeFile');
|
||||||
logger.info('Migrating cleanupChoices.closeFile');
|
set('cleanupChoices', { ...cleanupChoices, closeFile: true });
|
||||||
set('cleanupChoices', { ...cleanupChoices, closeFile: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
const { Menu } = require('electron');
|
const { Menu } = require('electron');
|
||||||
|
|
||||||
// https://github.com/electron/electron/issues/4068#issuecomment-274159726
|
// https://github.com/electron/electron/issues/4068#issuecomment-274159726
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
process.traceDeprecation = true;
|
process.traceDeprecation = true;
|
||||||
process.traceProcessWarnings = true;
|
process.traceProcessWarnings = true;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
const electron = require('electron');
|
const electron = require('electron');
|
||||||
const isDev = require('electron-is-dev');
|
const isDev = require('electron-is-dev');
|
||||||
const unhandled = require('electron-unhandled');
|
const unhandled = require('electron-unhandled');
|
||||||
@ -14,7 +15,7 @@ const { stat } = require('fs/promises');
|
|||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
const menu = require('./menu');
|
const menu = require('./menu');
|
||||||
const configStore = require('./configStore');
|
const configStore = require('./configStore');
|
||||||
const { frontendBuildDir, isLinux, isWindows } = require('./util');
|
const { frontendBuildDir, isLinux } = require('./util');
|
||||||
const attachContextMenu = require('./contextMenu');
|
const attachContextMenu = require('./contextMenu');
|
||||||
const HttpServer = require('./httpServer');
|
const HttpServer = require('./httpServer');
|
||||||
|
|
||||||
@ -298,6 +299,7 @@ function initApp() {
|
|||||||
// Call this immediately, to make sure we don't miss it (race condition)
|
// Call this immediately, to make sure we don't miss it (race condition)
|
||||||
const readyPromise = app.whenReady();
|
const readyPromise = app.whenReady();
|
||||||
|
|
||||||
|
// eslint-disable-next-line unicorn/prefer-top-level-await
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
logger.info('Initializing config store');
|
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
|
const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer'); // eslint-disable-line global-require,import/no-extraneous-dependencies
|
||||||
|
|
||||||
installExtension(REACT_DEVELOPER_TOOLS)
|
installExtension(REACT_DEVELOPER_TOOLS)
|
||||||
.then(name => logger.info('Added Extension', name))
|
.then((name) => logger.info('Added Extension', name))
|
||||||
.catch(err => logger.error('Failed to add extension', err));
|
.catch((err) => logger.error('Failed to add extension', err));
|
||||||
}
|
}
|
||||||
|
|
||||||
createWindow();
|
createWindow();
|
||||||
|
@ -18,8 +18,7 @@ function setCustomFfPath(path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getFfCommandLine(cmd, args) {
|
function getFfCommandLine(cmd, args) {
|
||||||
const mapArg = arg => (/[^0-9a-zA-Z-_]/.test(arg) ? `'${arg}'` : arg);
|
return `${cmd} ${args.map((arg) => (/[^\w-]/.test(arg) ? `'${arg}'` : arg)).join(' ')}`;
|
||||||
return `${cmd} ${args.map(mapArg).join(' ')}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFfPath(cmd) {
|
function getFfPath(cmd) {
|
||||||
@ -56,8 +55,10 @@ function handleProgress(process, durationIn, onProgress, customMatcher = () => u
|
|||||||
// console.log('progress', line);
|
// console.log('progress', line);
|
||||||
|
|
||||||
try {
|
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+/);
|
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 "
|
// 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) match = line.match(/(?:size|Lsize)=\s*[^\s]+\s+time=\s*([^\s]+)\s+/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
customMatcher(line);
|
customMatcher(line);
|
||||||
@ -108,7 +109,7 @@ function runFfmpegProcess(args, customExecaOptions, { logCli = true } = {}) {
|
|||||||
runningFfmpegs.add(process);
|
runningFfmpegs.add(process);
|
||||||
try {
|
try {
|
||||||
await process;
|
await process;
|
||||||
} catch (err) {
|
} catch {
|
||||||
// ignored here
|
// ignored here
|
||||||
} finally {
|
} finally {
|
||||||
runningFfmpegs.delete(process);
|
runningFfmpegs.delete(process);
|
||||||
@ -246,10 +247,11 @@ async function detectSceneChanges({ filePath, minChange, onProgress, from, to })
|
|||||||
handleProgress(process, to - from, onProgress);
|
handleProgress(process, to - from, onProgress);
|
||||||
const rl = readline.createInterface({ input: process.stdout });
|
const rl = readline.createInterface({ input: process.stdout });
|
||||||
rl.on('line', (line) => {
|
rl.on('line', (line) => {
|
||||||
|
// eslint-disable-next-line unicorn/better-regex
|
||||||
const match = line.match(/^frame:\d+\s+pts:\d+\s+pts_time:([\d.]+)/);
|
const match = line.match(/^frame:\d+\s+pts:\d+\s+pts_time:([\d.]+)/);
|
||||||
if (!match) return;
|
if (!match) return;
|
||||||
const time = parseFloat(match[1]);
|
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);
|
times.push(time);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -288,6 +290,7 @@ const mapFilterOptions = (options) => Object.entries(options).map(([key, value])
|
|||||||
|
|
||||||
async function blackDetect({ filePath, filterOptions, onProgress, from, to }) {
|
async function blackDetect({ filePath, filterOptions, onProgress, from, to }) {
|
||||||
function matchLineTokens(line) {
|
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\\.]+/);
|
const match = line.match(/^[blackdetect\s*@\s*0x[0-9a-f]+] black_start:([\d\\.]+) black_end:([\d\\.]+) black_duration:[\d\\.]+/);
|
||||||
if (!match) return {};
|
if (!match) return {};
|
||||||
return {
|
return {
|
||||||
@ -301,6 +304,7 @@ async function blackDetect({ filePath, filterOptions, onProgress, from, to }) {
|
|||||||
|
|
||||||
async function silenceDetect({ filePath, filterOptions, onProgress, from, to }) {
|
async function silenceDetect({ filePath, filterOptions, onProgress, from, to }) {
|
||||||
function matchLineTokens(line) {
|
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\\.]+)/);
|
const match = line.match(/^[silencedetect\s*@\s*0x[0-9a-f]+] silence_end: ([\d\\.]+)[|\s]+silence_duration: ([\d\\.]+)/);
|
||||||
if (!match) return {};
|
if (!match) return {};
|
||||||
const end = parseFloat(match[1]);
|
const end = parseFloat(match[1]);
|
||||||
@ -409,6 +413,7 @@ async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVi
|
|||||||
|
|
||||||
switch (video) {
|
switch (video) {
|
||||||
case 'hq': {
|
case 'hq': {
|
||||||
|
// eslint-disable-next-line unicorn/prefer-ternary
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
videoArgs = ['-vf', 'format=yuv420p', '-allow_sw', '1', '-vcodec', 'h264', '-b:v', '15M'];
|
videoArgs = ['-vf', 'format=yuv420p', '-allow_sw', '1', '-vcodec', 'h264', '-b:v', '15M'];
|
||||||
} else {
|
} else {
|
||||||
@ -423,6 +428,7 @@ async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVi
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'lq': {
|
case 'lq': {
|
||||||
|
// eslint-disable-next-line unicorn/prefer-ternary
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
videoArgs = ['-vf', `scale=-2:${targetHeight},format=yuv420p`, '-allow_sw', '1', '-sws_flags', 'lanczos', '-vcodec', 'h264', '-b:v', '1500k'];
|
videoArgs = ['-vf', `scale=-2:${targetHeight},format=yuv420p`, '-allow_sw', '1', '-sws_flags', 'lanczos', '-vcodec', 'h264', '-b:v', '1500k'];
|
||||||
} else {
|
} else {
|
||||||
@ -443,6 +449,7 @@ async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVi
|
|||||||
|
|
||||||
switch (audio) {
|
switch (audio) {
|
||||||
case 'hq': {
|
case 'hq': {
|
||||||
|
// eslint-disable-next-line unicorn/prefer-ternary
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
audioArgs = ['-acodec', 'aac_at', '-b:a', '192k'];
|
audioArgs = ['-acodec', 'aac_at', '-b:a', '192k'];
|
||||||
} else {
|
} else {
|
||||||
@ -451,6 +458,7 @@ async function html5ify({ outPath, filePath: filePathArg, speed, hasAudio, hasVi
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'lq': {
|
case 'lq': {
|
||||||
|
// eslint-disable-next-line unicorn/prefer-ternary
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
audioArgs = ['-acodec', 'aac_at', '-ar', '44100', '-ac', '2', '-b:a', '96k'];
|
audioArgs = ['-acodec', 'aac_at', '-ar', '44100', '-ac', '2', '-b:a', '96k'];
|
||||||
} else {
|
} else {
|
||||||
|
@ -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
|
// 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 LanguageDetector = window.require('i18next-electron-language-detector');
|
||||||
const isDev = require('electron-is-dev');
|
const isDev = require('electron-is-dev');
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
const { app } = require('electron');
|
const { app } = require('electron');
|
||||||
const { join } = require('path');
|
const { join } = require('path');
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const isDev = require('electron-is-dev');
|
const isDev = require('electron-is-dev');
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
const { app } = require('electron');
|
const { app } = require('electron');
|
||||||
const { join } = require('path');
|
const { join } = require('path');
|
||||||
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
const electron = require('electron');
|
const electron = require('electron');
|
||||||
const { t } = require('i18next');
|
const { t } = require('i18next');
|
||||||
|
|
||||||
// menu-safe i18n.t:
|
// menu-safe i18n.t:
|
||||||
// https://github.com/mifi/lossless-cut/issues/1456
|
// https://github.com/mifi/lossless-cut/issues/1456
|
||||||
const esc = (val) => val.replace(/&/g, '&&');
|
const esc = (val) => val.replaceAll('&', '&&');
|
||||||
|
|
||||||
const { Menu } = electron;
|
const { Menu } = electron;
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// eslint-disable-line unicorn/filename-case
|
||||||
const GitHub = require('github-api');
|
const GitHub = require('github-api');
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
const electron = require('electron');
|
const electron = require('electron');
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// eslint-disable-line unicorn/filename-case
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import icongen from 'icon-gen';
|
import icongen from 'icon-gen';
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// eslint-disable-line unicorn/filename-case
|
||||||
import execa from 'execa';
|
import execa from 'execa';
|
||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
|
|
||||||
|
50
src/App.tsx
50
src/App.tsx
@ -101,6 +101,7 @@ const videoStyle: CSSProperties = { width: '100%', height: '100%', objectFit: 'c
|
|||||||
const bottomStyle: CSSProperties = { background: controlsBackground, transition: darkModeTransition };
|
const bottomStyle: CSSProperties = { background: controlsBackground, transition: darkModeTransition };
|
||||||
|
|
||||||
const hevcPlaybackSupportedPromise = doesPlayerSupportHevcPlayback();
|
const hevcPlaybackSupportedPromise = doesPlayerSupportHevcPlayback();
|
||||||
|
// eslint-disable-next-line unicorn/prefer-top-level-await
|
||||||
hevcPlaybackSupportedPromise.catch((err) => console.error(err));
|
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) => {
|
const toggleCopyStreamId = useCallback((path, index) => {
|
||||||
setCopyStreamIdsForPath(path, (old) => ({ ...old, [index]: !old[index] }));
|
setCopyStreamIdsForPath(path, (old) => ({ ...old, [index]: !old[index] }));
|
||||||
@ -333,7 +334,7 @@ function App() {
|
|||||||
!!(copyStreamIdsByFile[path] || {})[streamId]
|
!!(copyStreamIdsByFile[path] || {})[streamId]
|
||||||
), [copyStreamIdsByFile]);
|
), [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 copyAnyAudioTrack = useMemo(() => checkCopyingAnyTrackOfType((stream) => stream.codec_type === 'audio'), [checkCopyingAnyTrackOfType]);
|
||||||
|
|
||||||
const subtitleStreams = useMemo(() => getSubtitleStreams(mainStreams), [mainStreams]);
|
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 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
|
// Streams that are not copy enabled by default
|
||||||
const extraStreams = useMemo(() => mainStreams.filter((stream) => !shouldCopyStreamByDefault(stream)), [mainStreams]);
|
const extraStreams = useMemo(() => mainStreams.filter((stream) => !shouldCopyStreamByDefault(stream)), [mainStreams]);
|
||||||
@ -678,7 +679,7 @@ function App() {
|
|||||||
|
|
||||||
function addThumbnail(thumbnail) {
|
function addThumbnail(thumbnail) {
|
||||||
// console.log('Rendered thumbnail', thumbnail.url);
|
// console.log('Rendered thumbnail', thumbnail.url);
|
||||||
setThumbnails(v => [...v, thumbnail]);
|
setThumbnails((v) => [...v, thumbnail]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasAudio = !!activeAudioStream;
|
const hasAudio = !!activeAudioStream;
|
||||||
@ -710,7 +711,7 @@ function App() {
|
|||||||
// Cleanup removed thumbnails
|
// Cleanup removed thumbnails
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
thumnailsRef.current.forEach((thumbnail) => {
|
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;
|
thumnailsRef.current = thumbnails;
|
||||||
}, [thumbnails]);
|
}, [thumbnails]);
|
||||||
@ -719,7 +720,7 @@ function App() {
|
|||||||
const subtitlesByStreamIdRef = useRef({});
|
const subtitlesByStreamIdRef = useRef({});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Object.values(thumnailsRef.current).forEach(({ url }) => {
|
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;
|
subtitlesByStreamIdRef.current = subtitlesByStreamId;
|
||||||
}, [subtitlesByStreamId]);
|
}, [subtitlesByStreamId]);
|
||||||
@ -732,7 +733,7 @@ function App() {
|
|||||||
|
|
||||||
const resetMergedOutFileName = useCallback(() => {
|
const resetMergedOutFileName = useCallback(() => {
|
||||||
const ext = getOutFileExtension({ isCustomFormatSelected, outFormat: fileFormat, filePath });
|
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);
|
setMergedOutFileName(outFileName);
|
||||||
}, [fileFormat, filePath, isCustomFormatSelected]);
|
}, [fileFormat, filePath, isCustomFormatSelected]);
|
||||||
|
|
||||||
@ -833,7 +834,7 @@ function App() {
|
|||||||
}, [html5ify, html5ifyDummy]);
|
}, [html5ify, html5ifyDummy]);
|
||||||
|
|
||||||
const convertFormatBatch = useCallback(async () => {
|
const convertFormatBatch = useCallback(async () => {
|
||||||
if (batchFiles.length < 1) return;
|
if (batchFiles.length === 0) return;
|
||||||
const filePaths = batchFiles.map((f) => f.path);
|
const filePaths = batchFiles.map((f) => f.path);
|
||||||
|
|
||||||
const failedFiles: string[] = [];
|
const failedFiles: string[] = [];
|
||||||
@ -1008,7 +1009,7 @@ function App() {
|
|||||||
externalFilesMeta,
|
externalFilesMeta,
|
||||||
mainStreams,
|
mainStreams,
|
||||||
copyStreamIdsByFile,
|
copyStreamIdsByFile,
|
||||||
cutSegments: cutSegments.map(s => ({ start: s.start, end: s.end })),
|
cutSegments: cutSegments.map((s) => ({ start: s.start, end: s.end })),
|
||||||
mainFileFormatData,
|
mainFileFormatData,
|
||||||
rotation,
|
rotation,
|
||||||
shortestFlag,
|
shortestFlag,
|
||||||
@ -1176,7 +1177,7 @@ function App() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (segmentsToExport.length < 1) {
|
if (segmentsToExport.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1526,6 +1527,7 @@ function App() {
|
|||||||
await html5ifyAndLoadWithPreferences(cod, fp, 'fastest', haveVideoStream, haveAudioStream);
|
await html5ifyAndLoadWithPreferences(cod, fp, 'fastest', haveVideoStream, haveAudioStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line unicorn/prefer-ternary
|
||||||
if (projectPath) {
|
if (projectPath) {
|
||||||
await loadEdlFile({ path: projectPath, type: 'llc' });
|
await loadEdlFile({ path: projectPath, type: 'llc' });
|
||||||
} else {
|
} else {
|
||||||
@ -1565,16 +1567,14 @@ function App() {
|
|||||||
// https://github.com/mifi/lossless-cut/issues/515
|
// https://github.com/mifi/lossless-cut/issues/515
|
||||||
setFilePath(fp);
|
setFilePath(fp);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err) {
|
if (err instanceof DirectoryAccessDeclinedError) return;
|
||||||
if (err instanceof DirectoryAccessDeclinedError) return;
|
|
||||||
}
|
|
||||||
resetState();
|
resetState();
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}, [storeProjectInWorkingDir, setWorking, loadEdlFile, getEdlFilePath, getEdlFilePathOld, enableAskForImportChapters, ensureAccessToSourceDir, loadCutSegments, autoLoadTimecode, enableNativeHevc, ensureWritableOutDir, customOutDir, resetState, setCopyStreamIdsForPath, setFileFormat, outFormatLocked, setDetectedFileFormat, html5ifyAndLoadWithPreferences, showPreviewFileLoadedMessage, showUnsupportedFileMessage, hideAllNotifications]);
|
}, [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 toggleLastCommands = useCallback(() => setLastCommandsVisible((val) => !val), []);
|
||||||
const toggleSettings = useCallback(() => setSettingsVisible(val => !val), []);
|
const toggleSettings = useCallback(() => setSettingsVisible((val) => !val), []);
|
||||||
|
|
||||||
const seekClosestKeyframe = useCallback((direction) => {
|
const seekClosestKeyframe = useCallback((direction) => {
|
||||||
const time = findNearestKeyFrameTime({ time: getRelevantTime(), direction });
|
const time = findNearestKeyFrameTime({ time: getRelevantTime(), direction });
|
||||||
@ -1822,7 +1822,7 @@ function App() {
|
|||||||
|
|
||||||
const userOpenFiles = useCallback(async (filePathsIn) => {
|
const userOpenFiles = useCallback(async (filePathsIn) => {
|
||||||
let filePaths = filePathsIn;
|
let filePaths = filePathsIn;
|
||||||
if (!filePaths || filePaths.length < 1) return;
|
if (!filePaths || filePaths.length === 0) return;
|
||||||
|
|
||||||
console.log('userOpenFiles');
|
console.log('userOpenFiles');
|
||||||
console.log(filePaths.join('\n'));
|
console.log(filePaths.join('\n'));
|
||||||
@ -1866,7 +1866,7 @@ function App() {
|
|||||||
const firstFilePath = filePaths[0];
|
const firstFilePath = filePaths[0];
|
||||||
|
|
||||||
// https://en.wikibooks.org/wiki/Inside_DVD-Video/Directory_Structure
|
// 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;
|
if (mustDisallowVob()) return;
|
||||||
filePaths = await readVideoTs(firstFilePath);
|
filePaths = await readVideoTs(firstFilePath);
|
||||||
}
|
}
|
||||||
@ -1975,7 +1975,7 @@ function App() {
|
|||||||
const showIncludeExternalStreamsDialog = useCallback(async () => {
|
const showIncludeExternalStreamsDialog = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile'] });
|
const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile'] });
|
||||||
if (canceled || filePaths.length < 1) return;
|
if (canceled || filePaths.length === 0) return;
|
||||||
await addStreamSourceFile(filePaths[0]);
|
await addStreamSourceFile(filePaths[0]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleError(err);
|
handleError(err);
|
||||||
@ -2031,9 +2031,9 @@ function App() {
|
|||||||
play: () => play(),
|
play: () => play(),
|
||||||
pause,
|
pause,
|
||||||
reducePlaybackRate: () => changePlaybackRate(-1),
|
reducePlaybackRate: () => changePlaybackRate(-1),
|
||||||
reducePlaybackRateMore: () => changePlaybackRate(-1, 2.0),
|
reducePlaybackRateMore: () => changePlaybackRate(-1, 2),
|
||||||
increasePlaybackRate: () => changePlaybackRate(1),
|
increasePlaybackRate: () => changePlaybackRate(1),
|
||||||
increasePlaybackRateMore: () => changePlaybackRate(1, 2.0),
|
increasePlaybackRateMore: () => changePlaybackRate(1, 2),
|
||||||
timelineToggleComfortZoom,
|
timelineToggleComfortZoom,
|
||||||
captureSnapshot,
|
captureSnapshot,
|
||||||
captureSnapshotAsCoverArt,
|
captureSnapshotAsCoverArt,
|
||||||
@ -2191,7 +2191,9 @@ function App() {
|
|||||||
useKeyboard({ keyBindings, onKeyPress });
|
useKeyboard({ keyBindings, onKeyPress });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
||||||
document.ondragover = dragPreventer;
|
document.ondragover = dragPreventer;
|
||||||
|
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
||||||
document.ondragend = dragPreventer;
|
document.ondragend = dragPreventer;
|
||||||
|
|
||||||
electron.ipcRenderer.send('renderer-ready');
|
electron.ipcRenderer.send('renderer-ready');
|
||||||
@ -2309,7 +2311,7 @@ function App() {
|
|||||||
// todo
|
// todo
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const actionsWithArgs: Record<string, (...args: any[]) => void> = {
|
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
|
// todo separate actions per type and move them into mainActions? https://github.com/mifi/lossless-cut/issues/254#issuecomment-932649424
|
||||||
importEdlFile,
|
importEdlFile,
|
||||||
exportEdlFile: tryExportEdlFile,
|
exportEdlFile: tryExportEdlFile,
|
||||||
@ -2351,7 +2353,7 @@ function App() {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (!ev.dataTransfer) return;
|
if (!ev.dataTransfer) return;
|
||||||
const { files } = ev.dataTransfer;
|
const { files } = ev.dataTransfer;
|
||||||
const filePaths = Array.from(files).map(f => f.path);
|
const filePaths = [...files].map((f) => f.path);
|
||||||
|
|
||||||
focusWindow();
|
focusWindow();
|
||||||
|
|
||||||
@ -2382,7 +2384,7 @@ function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const keyScrollPreventer = (e) => {
|
const keyScrollPreventer = (e) => {
|
||||||
// https://stackoverflow.com/questions/8916620/disable-arrow-key-scrolling-in-users-browser
|
// 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();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -2393,7 +2395,7 @@ function App() {
|
|||||||
|
|
||||||
const showLeftBar = batchFiles.length > 0;
|
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();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { FaTrashAlt, FaSave } from 'react-icons/fa';
|
import { FaTrashAlt, FaSave } from 'react-icons/fa';
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ import { askForPlaybackRate } from './dialogs';
|
|||||||
const { clipboard } = window.require('electron');
|
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;
|
const leftRightWidth = 100;
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ const InvertCutModeButton = memo(({ invertCutSegments, setInvertCutSegments }) =
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const onYinYangClick = useCallback(() => {
|
const onYinYangClick = useCallback(() => {
|
||||||
setInvertCutSegments(v => {
|
setInvertCutSegments((v) => {
|
||||||
const newVal = !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') });
|
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.') });
|
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)' }}
|
style={{ ...cutTimeInputStyle, color: isCutTimeManualSet() ? 'var(--red11)' : 'var(--gray12)' }}
|
||||||
type="text"
|
type="text"
|
||||||
title={isStart ? t('Manually input current segment\'s start time') : t('Manually input current segment\'s end time')}
|
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}
|
onPaste={handleCutTimePaste}
|
||||||
onBlur={() => setCutTimeManual()}
|
onBlur={() => setCutTimeManual()}
|
||||||
onContextMenu={handleContextMenu}
|
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>
|
<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>
|
<option key="" value="" disabled>{t('Zoom')}</option>
|
||||||
{zoomOptions.map(val => (
|
{zoomOptions.map((val) => (
|
||||||
<option key={val} value={String(val)}>{t('Zoom')} {val}x</option>
|
<option key={val} value={String(val)}>{t('Zoom')} {val}x</option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
@ -94,7 +94,7 @@ async function startPlayback({ path, video, videoStreamIndex, audioStreamIndex,
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sourceBuffer.buffered.length < 1) {
|
if (sourceBuffer.buffered.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,7 +239,9 @@ function drawJpegFrame(canvas: HTMLCanvasElement, jpegImage: Buffer) {
|
|||||||
console.error('Canvas context is null');
|
console.error('Canvas context is null');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
||||||
img.onload = () => ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
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.onerror = (error) => console.error('Canvas JPEG image error', error);
|
||||||
img.src = `data:image/jpeg;base64,${jpegImage.toString('base64')}`;
|
img.src = `data:image/jpeg;base64,${jpegImage.toString('base64')}`;
|
||||||
}
|
}
|
||||||
|
@ -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 { FaYinYang, FaSave, FaPlus, FaMinus, FaTag, FaSortNumericDown, FaAngleRight, FaRegCheckCircle, FaRegCircle } from 'react-icons/fa';
|
||||||
import { AiOutlineSplitCells } from 'react-icons/ai';
|
import { AiOutlineSplitCells } from 'react-icons/ai';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
@ -186,13 +186,9 @@ const SegmentList = memo(({
|
|||||||
|
|
||||||
let header = t('Segments to export:');
|
let header = t('Segments to export:');
|
||||||
if (segments.length === 0) {
|
if (segments.length === 0) {
|
||||||
if (invertCutSegments) {
|
header = invertCutSegments ? (
|
||||||
header = (
|
<Trans>You have enabled the "invert segments" 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>
|
||||||
<Trans>You have enabled the "invert segments" 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>
|
) : t('No segments to export.');
|
||||||
);
|
|
||||||
} else {
|
|
||||||
header = t('No segments to export.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onReorderSegs = useCallback(async (index) => {
|
const onReorderSegs = useCallback(async (index) => {
|
||||||
@ -285,6 +281,7 @@ const SegmentList = memo(({
|
|||||||
[tag]: value,
|
[tag]: value,
|
||||||
})), [setEditingSegmentTags]);
|
})), [setEditingSegmentTags]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const onTagReset = useCallback((tag) => setEditingSegmentTags(({ [tag]: deleted, ...rest }) => rest), [setEditingSegmentTags]);
|
const onTagReset = useCallback((tag) => setEditingSegmentTags(({ [tag]: deleted, ...rest }) => rest), [setEditingSegmentTags]);
|
||||||
|
|
||||||
const onSegmentTagsCloseComplete = useCallback(() => {
|
const onSegmentTagsCloseComplete = useCallback(() => {
|
||||||
|
@ -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 { FaImage, FaCheckCircle, FaPaperclip, FaVideo, FaVideoSlash, FaFileImport, FaVolumeUp, FaVolumeMute, FaBan, FaFileExport } from 'react-icons/fa';
|
||||||
import { GoFileBinary } from 'react-icons/go';
|
import { GoFileBinary } from 'react-icons/go';
|
||||||
@ -34,6 +34,7 @@ const EditFileDialog = memo(({ editingFile, allFilesMeta, customTagsByFile, setC
|
|||||||
|
|
||||||
const onTagReset = useCallback((tag) => {
|
const onTagReset = useCallback((tag) => {
|
||||||
setCustomTagsByFile((old) => {
|
setCustomTagsByFile((old) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { [tag]: deleted, ...rest } = old[editingFile] || {};
|
const { [tag]: deleted, ...rest } = old[editingFile] || {};
|
||||||
return { ...old, [editingFile]: rest };
|
return { ...old, [editingFile]: rest };
|
||||||
});
|
});
|
||||||
@ -101,7 +102,8 @@ const EditStreamDialog = memo(({ editingStream: { streamId: editingStreamId, pat
|
|||||||
const onTagReset = useCallback((tag) => {
|
const onTagReset = useCallback((tag) => {
|
||||||
updateStreamParams(editingFile, editingStreamId, (params) => {
|
updateStreamParams(editingFile, editingStreamId, (params) => {
|
||||||
if (!params.has('customTags')) return;
|
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];
|
delete params.get('customTags')[tag];
|
||||||
});
|
});
|
||||||
}, [editingFile, editingStreamId, updateStreamParams]);
|
}, [editingFile, editingStreamId, updateStreamParams]);
|
||||||
@ -140,6 +142,7 @@ const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copySt
|
|||||||
|
|
||||||
let Icon;
|
let Icon;
|
||||||
let codecTypeHuman;
|
let codecTypeHuman;
|
||||||
|
// eslint-disable-next-line unicorn/prefer-switch
|
||||||
if (stream.codec_type === 'audio') {
|
if (stream.codec_type === 'audio') {
|
||||||
Icon = copyStream ? FaVolumeUp : FaVolumeMute;
|
Icon = copyStream ? FaVolumeUp : FaVolumeMute;
|
||||||
codecTypeHuman = t('audio');
|
codecTypeHuman = t('audio');
|
||||||
@ -310,6 +313,7 @@ const StreamsSelector = memo(({
|
|||||||
async function removeFile(path) {
|
async function removeFile(path) {
|
||||||
setCopyStreamIdsForPath(path, () => ({}));
|
setCopyStreamIdsForPath(path, () => ({}));
|
||||||
setExternalFilesMeta((old) => {
|
setExternalFilesMeta((old) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { [path]: val, ...rest } = old;
|
const { [path]: val, ...rest } = old;
|
||||||
return rest;
|
return rest;
|
||||||
});
|
});
|
||||||
@ -318,6 +322,7 @@ const StreamsSelector = memo(({
|
|||||||
async function batchSetCopyStreamIdsForPath(path, streams, filter, enabled) {
|
async function batchSetCopyStreamIdsForPath(path, streams, filter, enabled) {
|
||||||
setCopyStreamIdsForPath(path, (old) => {
|
setCopyStreamIdsForPath(path, (old) => {
|
||||||
const ret = { ...old };
|
const ret = { ...old };
|
||||||
|
// eslint-disable-next-line unicorn/no-array-callback-reference
|
||||||
streams.filter(filter).forEach(({ index }) => {
|
streams.filter(filter).forEach(({ index }) => {
|
||||||
ret[index] = enabled;
|
ret[index] = enabled;
|
||||||
});
|
});
|
||||||
|
@ -245,6 +245,7 @@ const Timeline = memo(({
|
|||||||
style={{ position: 'relative', borderTop: '1px solid var(--gray7)', borderBottom: '1px solid var(--gray7)' }}
|
style={{ position: 'relative', borderTop: '1px solid var(--gray7)', borderBottom: '1px solid var(--gray7)' }}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onMouseMove={onMouseMove}
|
onMouseMove={onMouseMove}
|
||||||
|
// eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
|
||||||
onMouseOut={onMouseOut}
|
onMouseOut={onMouseOut}
|
||||||
>
|
>
|
||||||
{(waveformEnabled && !shouldShowWaveform) && (
|
{(waveformEnabled && !shouldShowWaveform) && (
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { memo, useCallback } from 'react';
|
import React, { memo, useCallback } from 'react';
|
||||||
import { IoIosSettings } from 'react-icons/io';
|
import { IoIosSettings } from 'react-icons/io';
|
||||||
import { FaLock, FaUnlock } from 'react-icons/fa';
|
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 { useTranslation } from 'react-i18next';
|
||||||
import Button from './components/Button';
|
import Button from './components/Button';
|
||||||
|
|
||||||
|
@ -52,14 +52,14 @@ const BigWaveform = memo(({ waveforms, relevantTime, playing, durationSafe, zoom
|
|||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const startTime = new Date().getTime();
|
const startTime = Date.now();
|
||||||
|
|
||||||
if (playing) {
|
if (playing) {
|
||||||
let raf;
|
let raf;
|
||||||
// eslint-disable-next-line no-inner-declarations
|
// eslint-disable-next-line no-inner-declarations
|
||||||
function render() {
|
function render() {
|
||||||
raf = window.requestAnimationFrame(() => {
|
raf = window.requestAnimationFrame(() => {
|
||||||
setSmoothTime(relevantTime + (new Date().getTime() - startTime) / 1000);
|
setSmoothTime(relevantTime + (Date.now() - startTime) / 1000);
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -25,12 +25,16 @@ const rowStyle: CSSProperties = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ConcatDialog = memo(({ isShown, onHide, paths, onConcat, alwaysConcatMultipleFiles, setAlwaysConcatMultipleFiles }: {
|
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,
|
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 { t } = useTranslation();
|
||||||
const { preserveMovData, setPreserveMovData, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge } = useUserSettings();
|
const { preserveMovData, setPreserveMovData, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge } = useUserSettings();
|
||||||
|
|
||||||
const [includeAllStreams, setIncludeAllStreams] = useState(false);
|
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 [fileMeta, setFileMeta] = useState<{ format: any, streams: any, chapters: any }>();
|
||||||
const [allFilesMetaCache, setAllFilesMetaCache] = useState({});
|
const [allFilesMetaCache, setAllFilesMetaCache] = useState({});
|
||||||
const [clearBatchFilesAfterConcat, setClearBatchFilesAfterConcat] = useState(false);
|
const [clearBatchFilesAfterConcat, setClearBatchFilesAfterConcat] = useState(false);
|
||||||
@ -62,7 +66,7 @@ const ConcatDialog = memo(({ isShown, onHide, paths, onConcat, alwaysConcatMulti
|
|||||||
setFileMeta(fileMetaNew);
|
setFileMeta(fileMetaNew);
|
||||||
setFileFormat(fileFormatNew);
|
setFileFormat(fileFormatNew);
|
||||||
setDetectedFileFormat(fileFormatNew);
|
setDetectedFileFormat(fileFormatNew);
|
||||||
setUniqueSuffix(new Date().getTime());
|
setUniqueSuffix(Date.now());
|
||||||
})().catch(console.error);
|
})().catch(console.error);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -24,7 +24,7 @@ const renderKeys = (keys) => keys.map((key, i) => (
|
|||||||
// From https://craig.is/killing/mice
|
// From https://craig.is/killing/mice
|
||||||
// For modifier keys you can use shift, ctrl, alt, or meta.
|
// For modifier keys you can use shift, ctrl, alt, or meta.
|
||||||
// You can substitute option for alt and command for 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) {
|
function fixKeys(keys) {
|
||||||
const replaced = keys.map((key) => {
|
const replaced = keys.map((key) => {
|
||||||
if (key === 'option') return 'alt';
|
if (key === 'option') return 'alt';
|
||||||
@ -32,10 +32,10 @@ function fixKeys(keys) {
|
|||||||
return key;
|
return key;
|
||||||
});
|
});
|
||||||
const uniqed = uniq(replaced);
|
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 === 0) return []; // only modifiers is invalid
|
||||||
if (nonModifierKeys.length > 1) return []; // can only have one non-modifier
|
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(({
|
const CreateBinding = memo(({
|
||||||
|
@ -27,7 +27,7 @@ const OutputFormatSelect = memo(({ style, detectedFileFormat, fileFormat, onOutp
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// 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>
|
<option key="disabled1" value="" disabled>{i18n.t('Output container format:')}</option>
|
||||||
|
|
||||||
{detectedFileFormat && (
|
{detectedFileFormat && (
|
||||||
|
@ -222,7 +222,8 @@ async function askForSegmentDuration(fileDuration) {
|
|||||||
// https://github.com/mifi/lossless-cut/issues/1153
|
// https://github.com/mifi/lossless-cut/issues/1153
|
||||||
async function askForSegmentsRandomDurationRange() {
|
async function askForSegmentsRandomDurationRange() {
|
||||||
function parse(str) {
|
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;
|
if (!match) return undefined;
|
||||||
const values = match.slice(1);
|
const values = match.slice(1);
|
||||||
const parsed = values.map((val) => parseFloat(val));
|
const parsed = values.map((val) => parseFloat(val));
|
||||||
@ -275,7 +276,7 @@ export async function askForShiftSegments() {
|
|||||||
let parseableValue = value;
|
let parseableValue = value;
|
||||||
let sign = 1;
|
let sign = 1;
|
||||||
if (parseableValue[0] === '-') {
|
if (parseableValue[0] === '-') {
|
||||||
parseableValue = parseableValue.substring(1);
|
parseableValue = parseableValue.slice(1);
|
||||||
sign = -1;
|
sign = -1;
|
||||||
}
|
}
|
||||||
const duration = parseDuration(parseableValue);
|
const duration = parseDuration(parseableValue);
|
||||||
|
@ -9,7 +9,7 @@ import { parseSrt, formatSrt, parseYouTube, formatYouTube, parseMplayerEdl, pars
|
|||||||
// eslint-disable-next-line no-underscore-dangle
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
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 readFixtureBinary = async (name: string) => fs.readFile(join(__dirname, 'fixtures', name), null);
|
||||||
|
|
||||||
const expectYouTube1 = [
|
const expectYouTube1 = [
|
||||||
@ -259,5 +259,5 @@ it('format srt', async () => {
|
|||||||
|
|
||||||
// https://github.com/mifi/lossless-cut/issues/1664
|
// https://github.com/mifi/lossless-cut/issues/1664
|
||||||
it('parses DV Analyzer Summary.txt', async () => {
|
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();
|
||||||
});
|
});
|
||||||
|
@ -21,10 +21,10 @@ export function getFrameCountRaw(detectedFps: number | undefined, sec: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseTime(str: string) {
|
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;
|
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 [, hourStr, minStr, secStr, msStr] = timeMatch;
|
||||||
const hour = hourStr != null ? parseInt(hourStr, 10) : 0;
|
const hour = hourStr != null ? parseInt(hourStr, 10) : 0;
|
||||||
@ -49,7 +49,7 @@ export const getFrameValParser = (fps) => (str) => {
|
|||||||
export async function parseCsv(csvStr, parseTimeFn) {
|
export async function parseCsv(csvStr, parseTimeFn) {
|
||||||
const rows = await csvParseAsync(csvStr, {});
|
const rows = await csvParseAsync(csvStr, {});
|
||||||
if (rows.length === 0) throw new Error(i18n.t('No rows found'));
|
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
|
const mapped = rows
|
||||||
.map(([start, end, name]) => ({
|
.map(([start, end, name]) => ({
|
||||||
@ -71,7 +71,7 @@ export async function parseCsv(csvStr, parseTimeFn) {
|
|||||||
|
|
||||||
export async function parseMplayerEdl(text) {
|
export async function parseMplayerEdl(text) {
|
||||||
const allRows = text.split('\n').map((line) => {
|
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;
|
if (!match) return undefined;
|
||||||
const start = parseFloat(match[1]);
|
const start = parseFloat(match[1]);
|
||||||
const end = parseFloat(match[2]);
|
const end = parseFloat(match[2]);
|
||||||
@ -79,7 +79,7 @@ export async function parseMplayerEdl(text) {
|
|||||||
if (Number.isNaN(start) || Number.isNaN(end)) return undefined;
|
if (Number.isNaN(start) || Number.isNaN(end)) return undefined;
|
||||||
if (start < 0 || end < 0 || start >= end) return undefined;
|
if (start < 0 || end < 0 || start >= end) return undefined;
|
||||||
return { start, end, type };
|
return { start, end, type };
|
||||||
}).filter((it) => it);
|
}).filter(Boolean);
|
||||||
|
|
||||||
const cutAwaySegments = allRows.filter((row) => row.type === 0);
|
const cutAwaySegments = allRows.filter((row) => row.type === 0);
|
||||||
const muteSegments = allRows.filter((row) => row.type === 1);
|
const muteSegments = allRows.filter((row) => row.type === 1);
|
||||||
@ -128,10 +128,10 @@ export function parseCuesheet(cuesheet) {
|
|||||||
export function parsePbf(buf: Buffer) {
|
export function parsePbf(buf: Buffer) {
|
||||||
const text = buf.toString('utf16le');
|
const text = buf.toString('utf16le');
|
||||||
const bookmarks = text.split('\n').map((line) => {
|
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] };
|
if (match) return { time: parseInt(match[1]!, 10) / 1000, name: match[2] };
|
||||||
return undefined;
|
return undefined;
|
||||||
}).filter((it) => it);
|
}).filter(Boolean);
|
||||||
|
|
||||||
const out: Segment[] = [];
|
const out: Segment[] = [];
|
||||||
|
|
||||||
@ -156,7 +156,7 @@ export function parseXmeml(xmlStr) {
|
|||||||
|
|
||||||
// TODO maybe support media.audio also?
|
// TODO maybe support media.audio also?
|
||||||
const { xmeml } = xml;
|
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;
|
let sequence;
|
||||||
|
|
||||||
@ -181,10 +181,10 @@ export function parseFcpXml(xmlStr) {
|
|||||||
const xml = new XMLParser({ ignoreAttributes: false }).parse(xmlStr);
|
const xml = new XMLParser({ ignoreAttributes: false }).parse(xmlStr);
|
||||||
|
|
||||||
const { fcpxml } = xml;
|
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) {
|
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');
|
if (!match) throw new Error('Invalid attribute');
|
||||||
return parseInt(match[1], 10) / parseInt(match[2], 10);
|
return parseInt(match[1], 10) / parseInt(match[2], 10);
|
||||||
}
|
}
|
||||||
@ -212,7 +212,7 @@ export function parseYouTube(str) {
|
|||||||
return { time, name };
|
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);
|
const linesSorted = sortBy(lines, (l) => l.time);
|
||||||
|
|
||||||
@ -275,7 +275,7 @@ export function parseDvAnalyzerSummaryTxt(txt: string) {
|
|||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (headerFound) {
|
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;
|
if (!match) break;
|
||||||
const h = parseInt(match[1]!, 10);
|
const h = parseInt(match[1]!, 10);
|
||||||
const m = parseInt(match[2]!, 10);
|
const m = parseInt(match[2]!, 10);
|
||||||
@ -325,7 +325,7 @@ export function parseSrt(text: string) {
|
|||||||
} else if (subtitleIndexAt != null && subtitleIndexAt > 0) {
|
} else if (subtitleIndexAt != null && subtitleIndexAt > 0) {
|
||||||
const match = line.match(/^(\d+:\d+:\d+[,.]\d+\s+)-->(\s+\d+:\d+:\d+[,.]\d+)$/);
|
const match = line.match(/^(\d+:\d+:\d+[,.]\d+\s+)-->(\s+\d+:\d+:\d+[,.]\d+)$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const fixComma = (v) => v.replace(/,/g, '.');
|
const fixComma = (v) => v.replaceAll(',', '.');
|
||||||
start = parseTime(fixComma(match[1]))?.time;
|
start = parseTime(fixComma(match[1]))?.time;
|
||||||
end = parseTime(fixComma(match[2]))?.time;
|
end = parseTime(fixComma(match[2]))?.time;
|
||||||
} else if (start != null && end != null) {
|
} else if (start != null && end != null) {
|
||||||
@ -345,5 +345,5 @@ export function parseSrt(text: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function formatSrt(segments) {
|
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`, '');
|
||||||
}
|
}
|
||||||
|
@ -13,24 +13,24 @@ const { basename } = window.require('path');
|
|||||||
const { dialog } = window.require('@electron/remote');
|
const { dialog } = window.require('@electron/remote');
|
||||||
|
|
||||||
export async function loadCsvSeconds(path) {
|
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) {
|
export async function loadCsvFrames(path, fps) {
|
||||||
if (!fps) throw new Error('The loaded file has an unknown framerate');
|
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) {
|
export async function loadXmeml(path) {
|
||||||
return parseXmeml(await readFile(path, 'utf-8'));
|
return parseXmeml(await readFile(path, 'utf8'));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadFcpXml(path) {
|
export async function loadFcpXml(path) {
|
||||||
return parseFcpXml(await readFile(path, 'utf-8'));
|
return parseFcpXml(await readFile(path, 'utf8'));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadDvAnalyzerSummaryTxt(path) {
|
export async function loadDvAnalyzerSummaryTxt(path) {
|
||||||
return parseDvAnalyzerSummaryTxt(await readFile(path, 'utf-8'));
|
return parseDvAnalyzerSummaryTxt(await readFile(path, 'utf8'));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadPbf(path) {
|
export async function loadPbf(path) {
|
||||||
@ -38,7 +38,7 @@ export async function loadPbf(path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function loadMplayerEdl(path) {
|
export async function loadMplayerEdl(path) {
|
||||||
return parseMplayerEdl(await readFile(path, 'utf-8'));
|
return parseMplayerEdl(await readFile(path, 'utf8'));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadCue(path) {
|
export async function loadCue(path) {
|
||||||
@ -46,7 +46,7 @@ export async function loadCue(path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function loadSrt(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) {
|
export async function saveCsv(path, cutSegments) {
|
||||||
@ -103,6 +103,7 @@ export async function askForEdlImport({ type, fps }: { type: EdlImportType, fps?
|
|||||||
if (type === 'youtube') return askForYouTubeInput();
|
if (type === 'youtube') return askForYouTubeInput();
|
||||||
|
|
||||||
let filters;
|
let filters;
|
||||||
|
// eslint-disable-next-line unicorn/prefer-switch
|
||||||
if (type === 'csv' || type === 'csv-frames') filters = [{ name: i18n.t('CSV files'), extensions: ['csv'] }];
|
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 === 'xmeml') filters = [{ name: i18n.t('XML files'), extensions: ['xml'] }];
|
||||||
else if (type === 'fcpxml') filters = [{ name: i18n.t('FCPXML files'), extensions: ['fcpxml'] }];
|
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'] }];
|
else if (type === 'llc') filters = [{ name: i18n.t('LosslessCut project'), extensions: ['llc'] }];
|
||||||
|
|
||||||
const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile'], filters });
|
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 });
|
return readEdlFile({ type, path: filePaths[0], fps });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,6 +124,7 @@ export async function exportEdlFile({ type, cutSegments, customOutDir, filePath,
|
|||||||
}) {
|
}) {
|
||||||
let filters;
|
let filters;
|
||||||
let ext;
|
let ext;
|
||||||
|
// eslint-disable-next-line unicorn/prefer-switch
|
||||||
if (type === 'csv') {
|
if (type === 'csv') {
|
||||||
ext = 'csv';
|
ext = 'csv';
|
||||||
filters = [{ name: i18n.t('CSV files'), extensions: [ext, 'txt'] }];
|
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 });
|
const { canceled, filePath: savePath } = await dialog.showSaveDialog({ defaultPath, filters });
|
||||||
if (canceled || !savePath) return;
|
if (canceled || !savePath) return;
|
||||||
console.log('Saving', type, savePath);
|
console.log('Saving', type, savePath);
|
||||||
|
// eslint-disable-next-line unicorn/prefer-switch
|
||||||
if (type === 'csv') await saveCsv(savePath, cutSegments);
|
if (type === 'csv') await saveCsv(savePath, cutSegments);
|
||||||
else if (type === 'tsv-human') await saveTsv(savePath, cutSegments);
|
else if (type === 'tsv-human') await saveTsv(savePath, cutSegments);
|
||||||
else if (type === 'csv-human') await saveCsvHuman(savePath, cutSegments);
|
else if (type === 'csv-human') await saveCsvHuman(savePath, cutSegments);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// eslint-disable-line unicorn/filename-case
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
|
|
||||||
export const blackdetect = () => ({
|
export const blackdetect = () => ({
|
||||||
|
@ -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 { 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 }) {
|
export function logStdoutStderr({ stdout, stderr }) {
|
||||||
if (stdout.length > 0) {
|
if (stdout.length > 0) {
|
||||||
@ -63,12 +68,12 @@ export async function readFrames({ filePath, from, to, streamIndex }) {
|
|||||||
// todo types
|
// todo types
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const packetsFiltered: Frame[] = (JSON.parse(stdout).packets as any[])
|
const packetsFiltered: Frame[] = (JSON.parse(stdout).packets as any[])
|
||||||
.map(p => ({
|
.map((p) => ({
|
||||||
keyframe: p.flags[0] === 'K',
|
keyframe: p.flags[0] === 'K',
|
||||||
time: parseFloat(p.pts_time),
|
time: parseFloat(p.pts_time),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
}))
|
}))
|
||||||
.filter(p => !Number.isNaN(p.time));
|
.filter((p) => !Number.isNaN(p.time));
|
||||||
|
|
||||||
return sortBy(packetsFiltered, 'time');
|
return sortBy(packetsFiltered, 'time');
|
||||||
}
|
}
|
||||||
@ -93,10 +98,18 @@ export type FindKeyframeMode = 'nearest' | 'before' | 'after';
|
|||||||
|
|
||||||
function findKeyframe(keyframes: Keyframe[], time: number, mode: FindKeyframeMode) {
|
function findKeyframe(keyframes: Keyframe[], time: number, mode: FindKeyframeMode) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'nearest': return findNearestKeyframe(keyframes, time);
|
case 'nearest': {
|
||||||
case 'before': return findPreviousKeyframe(keyframes, time);
|
return findNearestKeyframe(keyframes, time);
|
||||||
case 'after': return findNextKeyframe(keyframes, time);
|
}
|
||||||
default: return undefined;
|
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 (frames.length < 2) throw new Error(i18n.t('Less than 2 frames found'));
|
||||||
|
|
||||||
if (nextMode) {
|
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 === -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'));
|
if (index >= frames.length - 1) throw new Error(i18n.t('We are on the last frame'));
|
||||||
const { time } = frames[index];
|
const { time } = frames[index];
|
||||||
@ -136,12 +149,13 @@ export function getSafeCutTime(frames, cutTime, nextMode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const findReverseIndex = (arr, cb) => {
|
const findReverseIndex = (arr, cb) => {
|
||||||
|
// eslint-disable-next-line unicorn/no-array-callback-reference
|
||||||
const ret = [...arr].reverse().findIndex(cb);
|
const ret = [...arr].reverse().findIndex(cb);
|
||||||
if (ret === -1) return -1;
|
if (ret === -1) return -1;
|
||||||
return arr.length - 1 - ret;
|
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 === -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'));
|
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
|
// 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 === -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'));
|
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 }) {
|
export function findNearestKeyFrameTime({ frames, time, direction, fps }) {
|
||||||
const sigma = fps ? (1 / fps) : 0.1;
|
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;
|
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;
|
if (!nearestKeyFrame) return undefined;
|
||||||
return nearestKeyFrame.time;
|
return nearestKeyFrame.time;
|
||||||
}
|
}
|
||||||
@ -186,7 +200,7 @@ export async function tryMapChaptersToEdl(chapters) {
|
|||||||
end,
|
end,
|
||||||
name,
|
name,
|
||||||
};
|
};
|
||||||
}).filter((it) => it);
|
}).filter(Boolean);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to read chapters from file', err);
|
console.error('Failed to read chapters from file', err);
|
||||||
return [];
|
return [];
|
||||||
@ -218,6 +232,7 @@ export async function createChaptersFromSegments({ segmentPaths, chapterNames })
|
|||||||
function mapDefaultFormat({ streams, requestedFormat }) {
|
function mapDefaultFormat({ streams, requestedFormat }) {
|
||||||
if (requestedFormat === 'mp4') {
|
if (requestedFormat === 'mp4') {
|
||||||
// Only MOV supports these codecs, so default to MOV instead https://github.com/mifi/lossless-cut/issues/948
|
// 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))) {
|
if (streams.some((stream) => pcmAudioCodecs.includes(stream.codec_name))) {
|
||||||
return 'mov';
|
return 'mov';
|
||||||
}
|
}
|
||||||
@ -230,7 +245,7 @@ function mapDefaultFormat({ streams, requestedFormat }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function determineOutputFormat(ffprobeFormatsStr, filePath) {
|
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) {
|
if (ffprobeFormats.length === 0) {
|
||||||
console.warn('ffprobe returned unknown formats', ffprobeFormatsStr);
|
console.warn('ffprobe returned unknown formats', ffprobeFormatsStr);
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -256,19 +271,30 @@ async function determineOutputFormat(ffprobeFormatsStr, filePath) {
|
|||||||
// https://www.ftyps.com/
|
// https://www.ftyps.com/
|
||||||
// https://exiftool.org/TagNames/QuickTime.html
|
// https://exiftool.org/TagNames/QuickTime.html
|
||||||
switch (fileTypeResponse.mime) {
|
switch (fileTypeResponse.mime) {
|
||||||
case 'video/x-matroska': return 'matroska';
|
case 'video/x-matroska': {
|
||||||
case 'video/webm': return 'webm';
|
return 'matroska';
|
||||||
case 'video/quicktime': return 'mov';
|
}
|
||||||
case 'video/3gpp2': return '3g2';
|
case 'video/webm': {
|
||||||
case 'video/3gpp': return '3gp';
|
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
|
// 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 OutputFile2.m4a
|
||||||
// ffmpeg -i example.aac -c copy -f ipod OutputFile.m4a
|
// ffmpeg -i example.aac -c copy -f ipod OutputFile.m4a
|
||||||
// See also https://github.com/mifi/lossless-cut/issues/28
|
// See also https://github.com/mifi/lossless-cut/issues/28
|
||||||
case 'audio/x-m4a':
|
case 'audio/x-m4a':
|
||||||
case 'audio/mp4':
|
case 'audio/mp4': {
|
||||||
return 'ipod';
|
return 'ipod';
|
||||||
|
}
|
||||||
case 'image/avif':
|
case 'image/avif':
|
||||||
case 'image/heif':
|
case 'image/heif':
|
||||||
case 'image/heif-sequence':
|
case 'image/heif-sequence':
|
||||||
@ -276,8 +302,9 @@ async function determineOutputFormat(ffprobeFormatsStr, filePath) {
|
|||||||
case 'image/heic-sequence':
|
case 'image/heic-sequence':
|
||||||
case 'video/x-m4v':
|
case 'video/x-m4v':
|
||||||
case 'video/mp4':
|
case 'video/mp4':
|
||||||
case 'image/x-canon-cr3':
|
case 'image/x-canon-cr3': {
|
||||||
return 'mp4';
|
return 'mp4';
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
console.warn('file-type returned unknown format', ffprobeFormats, fileTypeResponse.mime);
|
console.warn('file-type returned unknown format', ffprobeFormats, fileTypeResponse.mime);
|
||||||
@ -303,7 +330,7 @@ export async function readFileMeta(filePath) {
|
|||||||
try {
|
try {
|
||||||
// https://github.com/mifi/lossless-cut/issues/1342
|
// https://github.com/mifi/lossless-cut/issues/1342
|
||||||
parsedJson = JSON.parse(stdout);
|
parsedJson = JSON.parse(stdout);
|
||||||
} catch (err) {
|
} catch {
|
||||||
console.log('ffprobe stdout', stdout);
|
console.log('ffprobe stdout', stdout);
|
||||||
throw new Error('ffprobe returned malformed data');
|
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 }) {
|
export async function renderThumbnails({ filePath, from, duration, onThumbnail }) {
|
||||||
// Time first render to determine how many to render
|
// 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);
|
let url = await renderThumbnail(filePath, from);
|
||||||
const endTime = new Date().getTime() / 1000;
|
const endTime = Date.now() / 1000;
|
||||||
onThumbnail({ time: from, url });
|
onThumbnail({ time: from, url });
|
||||||
|
|
||||||
// Aim for max 3 sec to render all
|
// Aim for max 3 sec to render all
|
||||||
const numThumbs = Math.floor(Math.min(Math.max(3 / (endTime - startTime), 3), 10));
|
const numThumbs = Math.floor(Math.min(Math.max(3 / (endTime - startTime), 3), 10));
|
||||||
// console.log(numThumbs);
|
// 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);
|
// console.log(thumbTimes);
|
||||||
|
|
||||||
await pMap(thumbTimes, async (time) => {
|
await pMap(thumbTimes, async (time) => {
|
||||||
@ -504,7 +531,7 @@ export async function extractWaveform({ filePath, outPath }) {
|
|||||||
const numSegs = 10;
|
const numSegs = 10;
|
||||||
const duration = 60 * 60;
|
const duration = 60 * 60;
|
||||||
const maxLen = 0.1;
|
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
|
// 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(';');
|
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) {
|
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;
|
if (!match) return undefined;
|
||||||
const num = parseInt(match[1], 10);
|
const num = parseInt(match[1], 10);
|
||||||
const den = parseInt(match[2], 10);
|
const den = parseInt(match[2], 10);
|
||||||
@ -555,6 +582,7 @@ export function getStreamFps(stream) {
|
|||||||
return fps;
|
return fps;
|
||||||
}
|
}
|
||||||
if (stream.codec_type === 'audio') {
|
if (stream.codec_type === 'audio') {
|
||||||
|
// eslint-disable-next-line unicorn/no-lonely-if
|
||||||
if (typeof stream.sample_rate === 'string') {
|
if (typeof stream.sample_rate === 'string') {
|
||||||
const sampleRate = parseInt(stream.sample_rate, 10);
|
const sampleRate = parseInt(stream.sample_rate, 10);
|
||||||
if (!Number.isNaN(sampleRate) && sampleRate > 0) {
|
if (!Number.isNaN(sampleRate) && sampleRate > 0) {
|
||||||
@ -595,7 +623,7 @@ export function getTimecodeFromStreams(streams) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
} catch (err) {
|
} catch {
|
||||||
// console.warn('Failed to parse timecode from file streams', err);
|
// console.warn('Failed to parse timecode from file streams', err);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
// Taken from: https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js
|
// Taken from: https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js
|
||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyright (c) 2015, Facebook, Inc.
|
* Copyright (c) 2015, Facebook, Inc.
|
||||||
@ -13,12 +12,10 @@
|
|||||||
* @typechecks
|
* @typechecks
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// Reasonable defaults
|
// Reasonable defaults
|
||||||
var PIXEL_STEP = 10;
|
const PIXEL_STEP = 10;
|
||||||
var LINE_HEIGHT = 40;
|
const LINE_HEIGHT = 40;
|
||||||
var PAGE_HEIGHT = 800;
|
const PAGE_HEIGHT = 800;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mouse wheel (and 2-finger trackpad) support on the web sucks. It is
|
* Mouse wheel (and 2-finger trackpad) support on the web sucks. It is
|
||||||
@ -120,18 +117,18 @@ var PAGE_HEIGHT = 800;
|
|||||||
* Firefox v4/Win7 | undefined | 3
|
* Firefox v4/Win7 | undefined | 3
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export default function normalizeWheel(/*object*/ event) /*object*/ {
|
export default function normalizeWheel(/* object */ event) /* object */ {
|
||||||
var sX = 0, sY = 0, // spinX, spinY
|
let sX = 0; let sY = 0; // spinX, spinY
|
||||||
pX = 0, pY = 0; // pixelX, pixelY
|
let pX = 0; let pY = 0; // pixelX, pixelY
|
||||||
|
|
||||||
// Legacy
|
// Legacy
|
||||||
if ('detail' in event) { sY = event.detail; }
|
if ('detail' in event) { sY = event.detail; }
|
||||||
if ('wheelDelta' in event) { sY = -event.wheelDelta / 120; }
|
if ('wheelDelta' in event) { sY = -event.wheelDelta / 120; }
|
||||||
if ('wheelDeltaY' in event) { sY = -event.wheelDeltaY / 120; }
|
if ('wheelDeltaY' in event) { sY = -event.wheelDeltaY / 120; }
|
||||||
if ('wheelDeltaX' in event) { sX = -event.wheelDeltaX / 120; }
|
if ('wheelDeltaX' in event) { sX = -event.wheelDeltaX / 120; }
|
||||||
|
|
||||||
// side scrolling on FF with DOMMouseScroll
|
// 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;
|
sX = sY;
|
||||||
sY = 0;
|
sY = 0;
|
||||||
}
|
}
|
||||||
@ -143,10 +140,10 @@ export default function normalizeWheel(/*object*/ event) /*object*/ {
|
|||||||
if ('deltaX' in event) { pX = event.deltaX; }
|
if ('deltaX' in event) { pX = event.deltaX; }
|
||||||
|
|
||||||
if ((pX || pY) && event.deltaMode) {
|
if ((pX || pY) && event.deltaMode) {
|
||||||
if (event.deltaMode == 1) { // delta in LINE units
|
if (event.deltaMode === 1) { // delta in LINE units
|
||||||
pX *= LINE_HEIGHT;
|
pX *= LINE_HEIGHT;
|
||||||
pY *= LINE_HEIGHT;
|
pY *= LINE_HEIGHT;
|
||||||
} else { // delta in PAGE units
|
} else { // delta in PAGE units
|
||||||
pX *= PAGE_HEIGHT;
|
pX *= PAGE_HEIGHT;
|
||||||
pY *= PAGE_HEIGHT;
|
pY *= PAGE_HEIGHT;
|
||||||
}
|
}
|
||||||
@ -156,8 +153,10 @@ export default function normalizeWheel(/*object*/ event) /*object*/ {
|
|||||||
if (pX && !sX) { sX = (pX < 1) ? -1 : 1; }
|
if (pX && !sX) { sX = (pX < 1) ? -1 : 1; }
|
||||||
if (pY && !sY) { sY = (pY < 1) ? -1 : 1; }
|
if (pY && !sY) { sY = (pY < 1) ? -1 : 1; }
|
||||||
|
|
||||||
return { spinX : sX,
|
return {
|
||||||
spinY : sY,
|
spinX: sX,
|
||||||
pixelX : pX,
|
spinY: sY,
|
||||||
pixelY : pY };
|
pixelX: pX,
|
||||||
|
pixelY: pY,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,12 @@ import { errorToast } from '../swal';
|
|||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import isDev from '../isDev';
|
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,
|
// 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
|
// except those exact file paths that have been explicitly drag-dropped into LosslessCut or opened using the opener dialog
|
||||||
|
@ -16,7 +16,7 @@ const { writeFile, mkdir } = window.require('fs/promises');
|
|||||||
async function writeChaptersFfmetadata(outDir, chapters) {
|
async function writeChaptersFfmetadata(outDir, chapters) {
|
||||||
if (!chapters || chapters.length === 0) return undefined;
|
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 }) => (
|
const ffmetadata = chapters.map(({ start, end, name }) => (
|
||||||
`[CHAPTER]\nTIMEBASE=1/1000\nSTART=${Math.floor(start * 1000)}\nEND=${Math.floor(end * 1000)}\ntitle=${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 (movFastStart) flags.push('+faststart');
|
||||||
|
|
||||||
if (flags.length === 0) return [];
|
if (flags.length === 0) return [];
|
||||||
return flatMap(flags, flag => ['-movflags', flag]);
|
return flatMap(flags, (flag) => ['-movflags', flag]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMatroskaFlags() {
|
function getMatroskaFlags() {
|
||||||
@ -153,7 +153,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
|
|||||||
// https://superuser.com/questions/787064/filename-quoting-in-ffmpeg-concat
|
// 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
|
// 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
|
// 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);
|
const ffmpegCommandLine = getFfCommandLine('ffmpeg', ffmpegArgs);
|
||||||
|
|
||||||
|
@ -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
|
// Also document.addEventListener needs custom handling of modifier keys or C will be triggered by CTRL+C, etc
|
||||||
import Mousetrap from 'mousetrap';
|
import Mousetrap from 'mousetrap';
|
||||||
|
|
||||||
const keyupActions = ['seekBackwards', 'seekForwards'];
|
const keyupActions = new Set(['seekBackwards', 'seekForwards']);
|
||||||
|
|
||||||
export default ({ keyBindings, onKeyPress: onKeyPressProp }) => {
|
export default ({ keyBindings, onKeyPress: onKeyPressProp }) => {
|
||||||
const onKeyPressRef = useRef();
|
const onKeyPressRef = useRef();
|
||||||
@ -25,7 +25,7 @@ export default ({ keyBindings, onKeyPress: onKeyPressProp }) => {
|
|||||||
keyBindings.forEach(({ action, keys }) => {
|
keyBindings.forEach(({ action, keys }) => {
|
||||||
mousetrap.bind(keys, () => onKeyPress({ action }));
|
mousetrap.bind(keys, () => onKeyPress({ action }));
|
||||||
|
|
||||||
if (keyupActions.includes(action)) {
|
if (keyupActions.has(action)) {
|
||||||
mousetrap.bind(keys, () => onKeyPress({ action, keyup: true }), 'keyup');
|
mousetrap.bind(keys, () => onKeyPress({ action, keyup: true }), 'keyup');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -117,7 +117,7 @@ export default ({
|
|||||||
const currentCutSeg = useMemo(() => cutSegments[currentSegIndexSafe], [currentSegIndexSafe, cutSegments]);
|
const currentCutSeg = useMemo(() => cutSegments[currentSegIndexSafe], [currentSegIndexSafe, cutSegments]);
|
||||||
const currentApparentCutSeg = useMemo(() => apparentCutSegments[currentSegIndexSafe], [apparentCutSegments, currentSegIndexSafe]);
|
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 detectBlackScenes = useCallback(async () => {
|
||||||
const filterOptions = await showParametersDialog({ title: i18n.t('Enter parameters'), parameters: ffmpegParameters.blackdetect(), docUrl: 'https://ffmpeg.org/ffmpeg-filters.html#blackdetect' });
|
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]);
|
}, [apparentCutSegments, duration, haveInvalidSegs]);
|
||||||
|
|
||||||
const invertAllSegments = useCallback(() => {
|
const invertAllSegments = useCallback(() => {
|
||||||
if (inverseCutSegments.length < 1) {
|
if (inverseCutSegments.length === 0) {
|
||||||
errorToast(i18n.t('Make sure you have no overlapping segments.'));
|
errorToast(i18n.t('Make sure you have no overlapping segments.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -180,7 +180,7 @@ export default ({
|
|||||||
}, [inverseCutSegments, setCutSegments]);
|
}, [inverseCutSegments, setCutSegments]);
|
||||||
|
|
||||||
const fillSegmentsGaps = useCallback(() => {
|
const fillSegmentsGaps = useCallback(() => {
|
||||||
if (inverseCutSegments.length < 1) {
|
if (inverseCutSegments.length === 0) {
|
||||||
errorToast(i18n.t('Make sure you have no overlapping segments.'));
|
errorToast(i18n.t('Make sure you have no overlapping segments.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -227,7 +227,7 @@ export default ({
|
|||||||
return newSegment;
|
return newSegment;
|
||||||
}, { concurrency });
|
}, { concurrency });
|
||||||
newSegments = newSegments.filter((segment) => segment.end > segment.start);
|
newSegments = newSegments.filter((segment) => segment.end > segment.start);
|
||||||
if (newSegments.length < 1) setCutSegments(createInitialCutSegments());
|
if (newSegments.length === 0) setCutSegments(createInitialCutSegments());
|
||||||
else setCutSegments(newSegments);
|
else setCutSegments(newSegments);
|
||||||
}, [apparentCutSegments, createInitialCutSegments, duration, isSegmentSelected, setCutSegments]);
|
}, [apparentCutSegments, createInitialCutSegments, duration, isSegmentSelected, setCutSegments]);
|
||||||
|
|
||||||
@ -449,7 +449,7 @@ export default ({
|
|||||||
}, [cutSegments, enableSegments]);
|
}, [cutSegments, enableSegments]);
|
||||||
|
|
||||||
const onLabelSelectedSegments = useCallback(async () => {
|
const onLabelSelectedSegments = useCallback(async () => {
|
||||||
if (selectedSegmentsRaw.length < 1) return;
|
if (selectedSegmentsRaw.length === 0) return;
|
||||||
const { name } = selectedSegmentsRaw[0];
|
const { name } = selectedSegmentsRaw[0];
|
||||||
const value = await labelSegmentDialog({ currentName: name, maxLength: maxLabelLength });
|
const value = await labelSegmentDialog({ currentName: name, maxLength: maxLabelLength });
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
|
@ -42,4 +42,5 @@ i18n
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line unicorn/prefer-export-from
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
@ -84,7 +84,7 @@ it('detects overlapping segments', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
expect(partitionIntoOverlappingRanges([
|
expect(partitionIntoOverlappingRanges([
|
||||||
{ start: 9, end: 10.50 },
|
{ start: 9, end: 10.5 },
|
||||||
{ start: 11, end: 12 },
|
{ start: 11, end: 12 },
|
||||||
{ start: 11.5, end: 12.5 },
|
{ start: 11.5, end: 12.5 },
|
||||||
{ start: 11.5, end: 13 },
|
{ start: 11.5, end: 13 },
|
||||||
|
@ -99,11 +99,11 @@ export function combineOverlappingSegments(existingSegments, getSegApparentEnd2)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return undefined; // then remove all other segments in this partition group
|
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) {
|
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 firstSegment = minBy(selectedSegments, (seg) => getSegApparentStart(seg));
|
||||||
const lastSegment = maxBy(selectedSegments, (seg) => getSegApparentEnd2(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
|
if (isSegmentSelected(existingSegment)) return undefined; // remove other selected segments
|
||||||
return existingSegment;
|
return existingSegment;
|
||||||
}).filter((segment) => segment);
|
}).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasAnySegmentOverlap(sortedSegments) {
|
export function hasAnySegmentOverlap(sortedSegments) {
|
||||||
if (sortedSegments.length < 1) return false;
|
if (sortedSegments.length === 0) return false;
|
||||||
|
|
||||||
const overlappingGroups = partitionIntoOverlappingRanges(sortedSegments);
|
const overlappingGroups = partitionIntoOverlappingRanges(sortedSegments);
|
||||||
return overlappingGroups.length > 0;
|
return overlappingGroups.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertSegments(sortedCutSegments, includeFirstSegment: boolean, includeLastSegment: boolean, duration?: number) {
|
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;
|
if (hasAnySegmentOverlap(sortedCutSegments)) return undefined;
|
||||||
|
|
||||||
@ -157,7 +157,7 @@ export function invertSegments(sortedCutSegments, includeFirstSegment: boolean,
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (includeLastSegment) {
|
if (includeLastSegment) {
|
||||||
const lastSeg = sortedCutSegments[sortedCutSegments.length - 1];
|
const lastSeg = sortedCutSegments.at(-1);
|
||||||
if (duration == null || lastSeg.end < duration) {
|
if (duration == null || lastSeg.end < duration) {
|
||||||
const inverted: InverseSegment = {
|
const inverted: InverseSegment = {
|
||||||
start: lastSeg.end,
|
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
|
// because chapters need to be contiguous, we need to insert gaps in-between
|
||||||
export function convertSegmentsToChapters(sortedSegments) {
|
export function convertSegmentsToChapters(sortedSegments) {
|
||||||
if (sortedSegments.length < 1) return [];
|
if (sortedSegments.length === 0) return [];
|
||||||
if (hasAnySegmentOverlap(sortedSegments)) throw new Error('Segments cannot overlap');
|
if (hasAnySegmentOverlap(sortedSegments)) throw new Error('Segments cannot overlap');
|
||||||
|
|
||||||
sortedSegments.map((segment) => ({ start: segment.start, end: segment.end, name: segment.name }));
|
sortedSegments.map((segment) => ({ start: segment.start, end: segment.end, name: segment.name }));
|
||||||
|
16
src/util.ts
16
src/util.ts
@ -163,12 +163,12 @@ export function handleError(arg1: unknown, arg2?: unknown) {
|
|||||||
toast.fire({
|
toast.fire({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: msg || i18n.t('An error has occurred.'),
|
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) {
|
export function filenamify(name) {
|
||||||
return name.replace(/[^0-9a-zA-Z_\-.]/g, '_');
|
return name.replaceAll(/[^\w.-]/g, '_');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withBlur(cb) {
|
export function withBlur(cb) {
|
||||||
@ -248,7 +248,7 @@ export async function findExistingHtml5FriendlyFile(fp, cod) {
|
|||||||
matches = [...matches, ...nonMatches];
|
matches = [...matches, ...nonMatches];
|
||||||
|
|
||||||
// console.log(matches);
|
// console.log(matches);
|
||||||
if (matches.length < 1) return undefined;
|
if (matches.length === 0) return undefined;
|
||||||
|
|
||||||
const { suffix, entry } = matches[0]!;
|
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
|
// eslint-disable-next-line no-useless-concat, one-var, one-var-declaration-per-line
|
||||||
const mf = 'mi' + 'fi.no', llc = 'Los' + 'slessC' + 'ut';
|
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 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
|
// example pathMatch: 37672NoveltyStudio.MediaConverter_9.0.6.0_x64__vjhnv588cyf84
|
||||||
if (!pathMatch) {
|
if (!pathMatch) {
|
||||||
console.warn('Unknown path match', appPath);
|
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
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
|
||||||
export function escapeRegExp(string) {
|
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;
|
export const readFileSize = async (path) => (await stat(path)).size;
|
||||||
@ -377,8 +378,7 @@ export function checkFileSizes(inputSize, outputSize) {
|
|||||||
|
|
||||||
function setDocumentExtraTitle(extra) {
|
function setDocumentExtraTitle(extra) {
|
||||||
const baseTitle = 'LosslessCut';
|
const baseTitle = 'LosslessCut';
|
||||||
if (extra != null) document.title = `${baseTitle} - ${extra}`;
|
document.title = extra != null ? `${baseTitle} - ${extra}` : baseTitle;
|
||||||
else document.title = 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 }) {
|
||||||
@ -402,7 +402,7 @@ export function mustDisallowVob() {
|
|||||||
|
|
||||||
export async function readVideoTs(videoTsPath) {
|
export async function readVideoTs(videoTsPath) {
|
||||||
const files = await readdir(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));
|
const ret = sortBy(relevantFiles).map((file) => join(videoTsPath, file));
|
||||||
if (ret.length === 0) throw new Error('No VTS vob files found in folder');
|
if (ret.length === 0) throw new Error('No VTS vob files found in folder');
|
||||||
return ret;
|
return ret;
|
||||||
|
@ -43,7 +43,8 @@ export const isExactDurationMatch = (str) => /^-?\d{2}:\d{2}:\d{2}.\d{3}$/.test(
|
|||||||
|
|
||||||
// See also parseYoutube
|
// See also parseYoutube
|
||||||
export function parseDuration(str) {
|
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;
|
if (!match) return undefined;
|
||||||
|
|
||||||
|
@ -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
|
// 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)
|
// 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() {
|
function getSegSuffix() {
|
||||||
if (name) return `-${filenamifyOrNot(name)}`;
|
if (name) return `-${filenamifyOrNot(name)}`;
|
||||||
@ -159,7 +159,7 @@ export function generateOutSegFileNames({ segments, template: desiredTemplate, f
|
|||||||
return [
|
return [
|
||||||
...rest,
|
...rest,
|
||||||
// If sanitation is enabled, make sure filename (last seg of the path) is not too long
|
// 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);
|
].join(pathSep);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// eslint-disable-line unicorn/filename-case
|
||||||
import clamp from 'lodash/clamp';
|
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
|
// 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).
|
// using a different multiplier (e.g., holding the shift key).
|
||||||
// https://github.com/mifi/lossless-cut/issues/447#issuecomment-766339083
|
// https://github.com/mifi/lossless-cut/issues/447#issuecomment-766339083
|
||||||
if ((newRate > 1.0 && playbackRate < 1.0) || (newRate < 1.0 && playbackRate > 1.0)) {
|
if ((newRate > 1 && playbackRate < 1) || (newRate < 1 && playbackRate > 1)) {
|
||||||
newRate = 1.0;
|
newRate = 1;
|
||||||
}
|
}
|
||||||
// And, clean up any rounding errors that get us to almost 1.0 (e.g., treat 1.00001 as 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)))) {
|
if ((newRate > (m ** (-1 / 2))) && (newRate < (m ** (1 / 2)))) {
|
||||||
newRate = 1.0;
|
newRate = 1;
|
||||||
}
|
}
|
||||||
return clamp(newRate, 0.1, 16);
|
return clamp(newRate, 0.1, 16);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// eslint-disable-line unicorn/filename-case
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
import { adjustRate, DEFAULT_PLAYBACK_RATE } from './rate-calculator';
|
import { adjustRate, DEFAULT_PLAYBACK_RATE } from './rate-calculator';
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
// https://www.ffmpeg.org/doxygen/3.2/libavutil_2utils_8c_source.html#l00079
|
// https://www.ffmpeg.org/doxygen/3.2/libavutil_2utils_8c_source.html#l00079
|
||||||
const defaultProcessedCodecTypes = [
|
const defaultProcessedCodecTypes = new Set([
|
||||||
'video',
|
'video',
|
||||||
'audio',
|
'audio',
|
||||||
'subtitle',
|
'subtitle',
|
||||||
'attachment',
|
'attachment',
|
||||||
];
|
]);
|
||||||
|
|
||||||
const unprocessableCodecs = [
|
const unprocessableCodecs = new Set([
|
||||||
'dvb_teletext', // ffmpeg doesn't seem to support this https://github.com/mifi/lossless-cut/issues/1343
|
'dvb_teletext', // ffmpeg doesn't seem to support this https://github.com/mifi/lossless-cut/issues/1343
|
||||||
];
|
]);
|
||||||
|
|
||||||
// taken from `ffmpeg -codecs`
|
// taken from `ffmpeg -codecs`
|
||||||
export const pcmAudioCodecs = [
|
export const pcmAudioCodecs = [
|
||||||
@ -122,6 +122,7 @@ function getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposi
|
|||||||
addArgs(`-c:${outputIndex}`, codec);
|
addArgs(`-c:${outputIndex}`, codec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line unicorn/prefer-switch
|
||||||
if (stream.codec_type === 'subtitle') {
|
if (stream.codec_type === 'subtitle') {
|
||||||
// mp4/mov only supports mov_text, so convert it https://stackoverflow.com/a/17584272/6519037
|
// mp4/mov only supports mov_text, so convert it https://stackoverflow.com/a/17584272/6519037
|
||||||
// https://github.com/mifi/lossless-cut/issues/418
|
// 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') {
|
} else if (outFormat === 'webm' && stream.codec_name === 'mov_text') {
|
||||||
// Only WebVTT subtitles are supported for WebM.
|
// Only WebVTT subtitles are supported for WebM.
|
||||||
addCodecArgs('webvtt');
|
addCodecArgs('webvtt');
|
||||||
|
// eslint-disable-next-line unicorn/prefer-switch
|
||||||
} else if (outFormat === 'srt') { // not technically lossless but why not
|
} else if (outFormat === 'srt') { // not technically lossless but why not
|
||||||
addCodecArgs('srt');
|
addCodecArgs('srt');
|
||||||
} else if (outFormat === 'ass') { // not technically lossless but why not
|
} else if (outFormat === 'ass') { // not technically lossless but why not
|
||||||
@ -171,6 +173,7 @@ function getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposi
|
|||||||
|
|
||||||
if (isMov(outFormat)) {
|
if (isMov(outFormat)) {
|
||||||
// 0x31766568 see https://github.com/mifi/lossless-cut/issues/1444
|
// 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') {
|
if (['0x0000', '0x31637668', '0x31766568'].includes(stream.codec_tag) && stream.codec_name === 'hevc') {
|
||||||
addArgs(`-tag:${outputIndex}`, 'hvc1');
|
addArgs(`-tag:${outputIndex}`, 'hvc1');
|
||||||
}
|
}
|
||||||
@ -214,8 +217,8 @@ export function getMapStreamsArgs({ startIndex = 0, outFormat, allFilesMeta, cop
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function shouldCopyStreamByDefault(stream) {
|
export function shouldCopyStreamByDefault(stream) {
|
||||||
if (!defaultProcessedCodecTypes.includes(stream.codec_type)) return false;
|
if (!defaultProcessedCodecTypes.has(stream.codec_type)) return false;
|
||||||
if (unprocessableCodecs.includes(stream.codec_name)) return false;
|
if (unprocessableCodecs.has(stream.codec_name)) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,9 +228,9 @@ export function isStreamThumbnail(stream) {
|
|||||||
return stream && stream.codec_type === 'video' && stream.disposition?.[attachedPicDisposition] === 1;
|
return stream && stream.codec_type === 'video' && stream.disposition?.[attachedPicDisposition] === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAudioStreams = (streams) => streams.filter(stream => stream.codec_type === 'audio');
|
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 getRealVideoStreams = (streams) => streams.filter((stream) => stream.codec_type === 'video' && !isStreamThumbnail(stream));
|
||||||
export const getSubtitleStreams = (streams) => streams.filter(stream => stream.codec_type === 'subtitle');
|
export const getSubtitleStreams = (streams) => streams.filter((stream) => stream.codec_type === 'subtitle');
|
||||||
|
|
||||||
// videoTracks/audioTracks seems to be 1-indexed, while ffmpeg is 0-indexes
|
// videoTracks/audioTracks seems to be 1-indexed, while ffmpeg is 0-indexes
|
||||||
const getHtml5TrackId = (ffmpegTrackIndex: number) => String(ffmpegTrackIndex + 1);
|
const getHtml5TrackId = (ffmpegTrackIndex: number) => String(ffmpegTrackIndex + 1);
|
||||||
|
Loading…
Reference in New Issue
Block a user