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