2020-03-19 15:37:38 +00:00
import i18n from 'i18next' ;
2021-11-15 06:15:09 +00:00
import pMap from 'p-map' ;
2023-01-02 09:17:41 +00:00
import ky from 'ky' ;
2023-02-05 09:33:24 +00:00
import prettyBytes from 'pretty-bytes' ;
2023-09-21 13:53:41 +00:00
import sortBy from 'lodash/sortBy' ;
2024-03-02 17:12:35 +00:00
import pRetry , { Options } from 'p-retry' ;
2024-02-12 16:01:43 +00:00
import { ExecaError } from 'execa' ;
2024-03-02 17:12:35 +00:00
import type * as FsPromises from 'fs/promises' ;
import type * as Os from 'os' ;
import type * as FsExtra from 'fs-extra' ;
import type { PlatformPath } from 'path' ;
2023-01-02 09:17:41 +00:00
import isDev from './isDev' ;
2023-02-16 05:14:15 +00:00
import Swal , { toast } from './swal' ;
2023-12-22 05:43:22 +00:00
import { ffmpegExtractWindow } from './util/constants' ;
2016-11-03 21:31:13 +00:00
2024-03-02 17:12:35 +00:00
const { dirname , parse : parsePath , join , extname , isAbsolute , resolve , basename } : PlatformPath = window . require ( 'path' ) ;
const fsExtra : typeof FsExtra = window . require ( 'fs-extra' ) ;
const { stat , lstat , readdir , utimes , unlink } : typeof FsPromises = window . require ( 'fs/promises' ) ;
const os : typeof Os = window . require ( 'os' ) ;
2023-01-15 09:55:14 +00:00
const { ipcRenderer } = window . require ( 'electron' ) ;
2023-01-02 09:17:41 +00:00
const remote = window . require ( '@electron/remote' ) ;
2020-03-04 10:41:40 +00:00
2023-01-02 09:17:41 +00:00
2024-02-14 13:12:16 +00:00
const trashFile = async ( path : string ) = > ipcRenderer . invoke ( 'tryTrashItem' , path ) ;
2022-11-22 15:28:49 +00:00
2024-02-14 13:12:16 +00:00
export const showItemInFolder = async ( path : string ) = > ipcRenderer . invoke ( 'showItemInFolder' , path ) ;
2023-12-02 12:32:27 +00:00
2024-02-12 06:11:36 +00:00
export function getFileDir ( filePath? : string ) {
2022-02-20 16:04:12 +00:00
return filePath ? dirname ( filePath ) : undefined ;
}
2024-03-02 17:12:35 +00:00
export function getOutDir < T1 extends string | undefined , T2 extends string | undefined > ( customOutDir? : T1 , filePath? : T2 ) : T1 extends string ? string : T2 extends string ? string : undefined ;
export function getOutDir ( customOutDir? : string | undefined , filePath? : string | undefined ) {
if ( customOutDir != null ) return customOutDir ;
if ( filePath != null ) return getFileDir ( filePath ) ;
2020-02-20 17:57:14 +00:00
return undefined ;
2020-02-15 07:57:40 +00:00
}
2024-02-14 13:12:16 +00:00
function getFileBaseName ( filePath? : string ) {
2020-02-20 17:57:14 +00:00
if ( ! filePath ) return undefined ;
2021-08-26 16:58:38 +00:00
const parsed = parsePath ( filePath ) ;
2021-04-01 05:47:18 +00:00
return parsed . name ;
}
2017-08-14 12:25:27 +00:00
2024-03-03 12:35:04 +00:00
export function getOutPath < T extends string | undefined > ( a : { customOutDir? : string | undefined , filePath? : T | undefined , fileName : string } ) : T extends string ? string : undefined ;
export function getOutPath ( { customOutDir , filePath , fileName } : { customOutDir? : string | undefined , filePath? : string | undefined , fileName : string } ) {
2024-03-02 17:12:35 +00:00
if ( filePath == null ) return undefined ;
2022-09-11 20:53:00 +00:00
return join ( getOutDir ( customOutDir , filePath ) , fileName ) ;
}
2024-02-14 13:12:16 +00:00
export const getSuffixedFileName = ( filePath : string | undefined , nameSuffix : string ) = > ` ${ getFileBaseName ( filePath ) } - ${ nameSuffix } ` ;
2023-01-06 14:24:14 +00:00
2024-03-03 12:35:04 +00:00
export function getSuffixedOutPath < T extends string | undefined > ( a : { customOutDir? : string | undefined , filePath? : T | undefined , nameSuffix : string } ) : T extends string ? string : undefined ;
export function getSuffixedOutPath ( { customOutDir , filePath , nameSuffix } : { customOutDir? : string | undefined , filePath? : string | undefined , nameSuffix : string } ) {
2024-03-02 17:12:35 +00:00
if ( filePath == null ) return undefined ;
2023-01-06 14:24:14 +00:00
return getOutPath ( { customOutDir , filePath , fileName : getSuffixedFileName ( filePath , nameSuffix ) } ) ;
2017-08-14 12:25:27 +00:00
}
2024-02-14 13:12:16 +00:00
export async function havePermissionToReadFile ( filePath : string ) {
2021-02-06 22:02:39 +00:00
try {
2023-02-05 09:33:24 +00:00
const fd = await fsExtra . open ( filePath , 'r' ) ;
2021-02-06 22:02:39 +00:00
try {
2023-02-05 09:33:24 +00:00
await fsExtra . close ( fd ) ;
2021-02-06 22:02:39 +00:00
} catch ( err ) {
console . error ( 'Failed to close fd' , err ) ;
}
} catch ( err ) {
2024-02-12 06:11:36 +00:00
if ( err instanceof Error && 'code' in err && [ 'EPERM' , 'EACCES' ] . includes ( err . code as string ) ) return false ;
2021-02-06 22:02:39 +00:00
console . error ( err ) ;
}
return true ;
}
2024-03-15 13:45:33 +00:00
export async function checkDirWriteAccess ( dirPath : string ) {
2020-03-31 05:11:55 +00:00
try {
2023-02-05 09:33:24 +00:00
await fsExtra . access ( dirPath , fsExtra . constants . W_OK ) ;
2020-03-31 05:11:55 +00:00
} catch ( err ) {
2024-02-12 06:11:36 +00:00
if ( err instanceof Error && 'code' in err ) {
if ( err . code === 'EPERM' ) return false ; // Thrown on Mac (MAS build) when user has not yet allowed access
if ( err . code === 'EACCES' ) return false ; // Thrown on Linux when user doesn't have access to output dir
}
2020-03-31 05:11:55 +00:00
console . error ( err ) ;
}
return true ;
}
2024-03-15 13:45:33 +00:00
export async function pathExists ( pathIn : string ) {
2023-02-05 09:33:24 +00:00
return fsExtra . pathExists ( pathIn ) ;
2021-08-24 09:21:26 +00:00
}
2024-02-12 06:11:36 +00:00
export async function getPathReadAccessError ( pathIn : string ) {
2022-02-20 13:11:55 +00:00
try {
2023-02-05 09:33:24 +00:00
await fsExtra . access ( pathIn , fsExtra . constants . R_OK ) ;
2022-02-20 13:11:55 +00:00
return undefined ;
} catch ( err ) {
2024-02-12 06:11:36 +00:00
return err instanceof Error && 'code' in err && typeof err . code === 'string' ? err.code : undefined ;
2022-02-20 13:11:55 +00:00
}
}
2024-03-15 13:45:33 +00:00
export async function dirExists ( dirPath : string ) {
2024-01-31 14:59:54 +00:00
return ( await pathExists ( dirPath ) ) && ( await lstat ( dirPath ) ) . isDirectory ( ) ;
2020-04-06 16:08:46 +00:00
}
2023-12-31 03:34:15 +00:00
// const testFailFsOperation = isDev;
const testFailFsOperation = false ;
// Retry because sometimes write operations fail on windows due to the file being locked for various reasons (often anti-virus) #272 #1797 #1704
2024-03-15 13:45:33 +00:00
export async function fsOperationWithRetry ( operation : ( ) = > Promise < unknown > , { signal , retries = 10 , minTimeout = 100 , maxTimeout = 2000 , . . . opts } : Options & { retries? : number | undefined , minTimeout? : number | undefined , maxTimeout? : number | undefined } = { } ) {
2023-12-31 03:34:15 +00:00
return pRetry ( async ( ) = > {
if ( testFailFsOperation && Math . random ( ) > 0.3 ) throw Object . assign ( new Error ( 'test delete failure' ) , { code : 'EPERM' } ) ;
await operation ( ) ;
2024-03-03 12:35:04 +00:00
// @ts-expect-error todo
2023-12-31 03:34:15 +00:00
} , {
retries ,
signal ,
minTimeout ,
maxTimeout ,
// mimic fs.rm `maxRetries` https://nodejs.org/api/fs.html#fspromisesrmpath-options
2024-02-12 06:11:36 +00:00
shouldRetry : ( err ) = > err instanceof Error && 'code' in err && typeof err . code === 'string' && [ 'EBUSY' , 'EMFILE' , 'ENFILE' , 'EPERM' ] . includes ( err . code ) ,
2023-12-31 03:34:15 +00:00
. . . opts ,
} ) ;
}
// example error: index-18074aaf.js:166 Failed to delete C:\Users\USERNAME\Desktop\RC\New folder\2023-12-27 21-45-22 (GMT p5)-merged-1703933052361-00.01.04.915-00.01.07.424-seg1.mp4 Error: EPERM: operation not permitted, unlink 'C:\Users\USERNAME\Desktop\RC\New folder\2023-12-27 21-45-22 (GMT p5)-merged-1703933052361-00.01.04.915-00.01.07.424-seg1.mp4'
2024-03-02 17:12:35 +00:00
export const unlinkWithRetry = async ( path : string , options? : Options ) = > fsOperationWithRetry ( async ( ) = > unlink ( path ) , { . . . options , onFailedAttempt : ( error ) = > console . warn ( 'Retrying delete' , path , error . attemptNumber ) } ) ;
2023-12-31 03:34:15 +00:00
// example error: index-18074aaf.js:160 Error: EPERM: operation not permitted, utime 'C:\Users\USERNAME\Desktop\RC\New folder\2023-12-27 21-45-22 (GMT p5)-merged-1703933052361-cut-merged-1703933070237.mp4'
2024-03-02 17:12:35 +00:00
export const utimesWithRetry = async ( path : string , atime : number , mtime : number , options? : Options ) = > fsOperationWithRetry ( async ( ) = > utimes ( path , atime , mtime ) , { . . . options , onFailedAttempt : ( error ) = > console . warn ( 'Retrying utimes' , path , error . attemptNumber ) } ) ;
2023-12-31 03:34:15 +00:00
2024-02-14 13:12:16 +00:00
export const getFrameDuration = ( fps? : number ) = > 1 / ( fps ? ? 30 ) ;
2024-03-15 13:45:33 +00:00
export async function transferTimestamps ( { inPath , outPath , cutFrom = 0 , cutTo = 0 , duration = 0 , treatInputFileModifiedTimeAsStart = true , treatOutputFileModifiedTimeAsStart } : {
inPath : string , outPath : string , cutFrom? : number | undefined , cutTo? : number | undefined , duration? : number | undefined , treatInputFileModifiedTimeAsStart? : boolean , treatOutputFileModifiedTimeAsStart : boolean | null | undefined
} ) {
2023-08-20 13:32:02 +00:00
if ( treatOutputFileModifiedTimeAsStart == null ) return ; // null means disabled;
// see https://github.com/mifi/lossless-cut/issues/1017#issuecomment-1049097115
2024-03-15 13:45:33 +00:00
function calculateTime ( fileTime : number ) {
2023-08-20 13:32:02 +00:00
if ( treatInputFileModifiedTimeAsStart && treatOutputFileModifiedTimeAsStart ) {
return fileTime + cutFrom ;
}
if ( ! treatInputFileModifiedTimeAsStart && ! treatOutputFileModifiedTimeAsStart ) {
return fileTime - duration + cutTo ;
}
if ( treatInputFileModifiedTimeAsStart && ! treatOutputFileModifiedTimeAsStart ) {
return fileTime + cutTo ;
}
// if (!treatInputFileModifiedTimeAsStart && treatOutputFileModifiedTimeAsStart) {
return fileTime - duration + cutFrom ;
}
2017-09-14 17:15:03 +00:00
try {
2023-02-05 09:33:24 +00:00
const { atime , mtime } = await stat ( inPath ) ;
2023-12-31 03:34:15 +00:00
await utimesWithRetry ( outPath , calculateTime ( ( atime . getTime ( ) / 1000 ) ) , calculateTime ( ( mtime . getTime ( ) / 1000 ) ) ) ;
2018-02-11 13:09:01 +00:00
} catch ( err ) {
console . error ( 'Failed to set output file modified time' , err ) ;
}
}
2024-02-12 06:11:36 +00:00
export function handleError ( arg1 : unknown , arg2? : unknown ) {
2021-08-26 16:58:38 +00:00
console . error ( 'handleError' , arg1 , arg2 ) ;
let msg ;
let errorMsg ;
if ( typeof arg1 === 'string' ) msg = arg1 ;
else if ( typeof arg2 === 'string' ) msg = arg2 ;
if ( arg1 instanceof Error ) errorMsg = arg1 . message ;
if ( arg2 instanceof Error ) errorMsg = arg2 . message ;
2021-08-25 15:21:30 +00:00
toast . fire ( {
icon : 'error' ,
2021-08-26 16:58:38 +00:00
title : msg || i18n . t ( 'An error has occurred.' ) ,
2024-02-20 14:48:40 +00:00
text : errorMsg ? errorMsg . slice ( 0 , 300 ) : undefined ,
2021-08-25 15:21:30 +00:00
} ) ;
}
2024-03-03 09:27:46 +00:00
export function filenamify ( name : string ) {
2024-02-20 14:48:40 +00:00
return name . replaceAll ( /[^\w.-]/g , '_' ) ;
2020-02-24 09:04:55 +00:00
}
2020-03-04 10:41:40 +00:00
export function withBlur ( cb ) {
2020-02-26 03:11:28 +00:00
return ( e ) = > {
cb ( e ) ;
2023-12-05 05:00:54 +00:00
e . target ? . blur ( ) ;
2020-02-26 03:11:28 +00:00
} ;
}
2020-11-21 11:36:39 +00:00
export function dragPreventer ( ev ) {
ev . preventDefault ( ) ;
}
2020-04-10 14:04:19 +00:00
export const isMasBuild = window . process . mas ;
2020-12-08 15:38:52 +00:00
export const isWindowsStoreBuild = window . process . windowsStore ;
export const isStoreBuild = isMasBuild || isWindowsStoreBuild ;
2020-11-22 22:39:39 +00:00
2021-11-13 11:26:32 +00:00
export const platform = os . platform ( ) ;
2022-03-09 15:40:09 +00:00
export const arch = os . arch ( ) ;
2020-11-26 11:50:47 +00:00
export const isWindows = platform === 'win32' ;
2021-03-31 08:14:58 +00:00
export const isMac = platform === 'darwin' ;
2021-01-23 18:02:33 +00:00
2024-03-02 17:12:35 +00:00
export function getExtensionForFormat ( format : string ) {
2021-01-23 18:02:33 +00:00
const ext = {
matroska : 'mkv' ,
ipod : 'm4a' ,
2023-01-03 09:45:58 +00:00
adts : 'aac' ,
2023-08-20 21:15:19 +00:00
mpegts : 'ts' ,
2021-01-23 18:02:33 +00:00
} [ format ] ;
return ext || format ;
}
2024-03-02 17:12:35 +00:00
export function getOutFileExtension ( { isCustomFormatSelected , outFormat , filePath } : {
isCustomFormatSelected? : boolean , outFormat : string , filePath : string ,
} ) {
2022-03-18 07:40:08 +00:00
if ( ! isCustomFormatSelected ) {
const ext = extname ( filePath ) ;
// QuickTime is quirky about the file extension of mov files (has to be .mov)
// https://github.com/mifi/lossless-cut/issues/1075#issuecomment-1072084286
const hasMovIncorrectExtension = outFormat === 'mov' && ext . toLowerCase ( ) !== '.mov' ;
// OK, just keep the current extension. Because most players will not care about the extension
if ( ! hasMovIncorrectExtension ) return extname ( filePath ) ;
}
2023-01-03 09:45:58 +00:00
// user is changing format, must update extension too
2022-03-18 07:40:08 +00:00
return ` . ${ getExtensionForFormat ( outFormat ) } ` ;
2021-01-23 18:02:33 +00:00
}
export const hasDuplicates = ( arr ) = > new Set ( arr ) . size !== arr . length ;
2021-08-24 09:21:26 +00:00
// Need to resolve relative paths from the command line https://github.com/mifi/lossless-cut/issues/639
2024-02-12 06:11:36 +00:00
export const resolvePathIfNeeded = ( inPath : string ) = > ( isAbsolute ( inPath ) ? inPath : resolve ( inPath ) ) ;
2021-08-26 16:58:38 +00:00
export const html5ifiedPrefix = 'html5ified-' ;
export const html5dummySuffix = 'dummy' ;
export async function findExistingHtml5FriendlyFile ( fp , cod ) {
// The order is the priority we will search:
2024-01-04 15:33:33 +00:00
const suffixes = [ 'slowest' , 'slow-audio' , 'slow' , 'fast-audio-remux' , 'fast-audio' , 'fast' , html5dummySuffix ] ;
2023-01-06 14:24:14 +00:00
const prefix = getSuffixedFileName ( fp , html5ifiedPrefix ) ;
2021-08-26 16:58:38 +00:00
const outDir = getOutDir ( cod , fp ) ;
2024-03-02 17:12:35 +00:00
if ( outDir == null ) throw new Error ( ) ;
2021-08-26 16:58:38 +00:00
const dirEntries = await readdir ( outDir ) ;
const html5ifiedDirEntries = dirEntries . filter ( ( entry ) = > entry . startsWith ( prefix ) ) ;
2024-03-02 17:12:35 +00:00
let matches : { entry : string , suffix? : string } [ ] = [ ] ;
2021-08-26 16:58:38 +00:00
suffixes . forEach ( ( suffix ) = > {
const entryWithSuffix = html5ifiedDirEntries . find ( ( entry ) = > new RegExp ( ` ${ suffix } \\ ..* $ ` ) . test ( entry . replace ( prefix , '' ) ) ) ;
if ( entryWithSuffix ) matches = [ . . . matches , { entry : entryWithSuffix , suffix } ] ;
} ) ;
const nonMatches = html5ifiedDirEntries . filter ( ( entry ) = > ! matches . some ( ( m ) = > m . entry === entry ) ) . map ( ( entry ) = > ( { entry } ) ) ;
// Allow for non-suffix matches too, e.g. user has a custom html5ified- file but with none of the suffixes above (but last priority)
matches = [ . . . matches , . . . nonMatches ] ;
// console.log(matches);
2024-02-20 14:48:40 +00:00
if ( matches . length === 0 ) return undefined ;
2021-08-26 16:58:38 +00:00
2024-02-12 06:11:36 +00:00
const { suffix , entry } = matches [ 0 ] ! ;
2021-08-26 16:58:38 +00:00
return {
path : join ( outDir , entry ) ,
2024-01-04 15:33:33 +00:00
usingDummyVideo : suffix === html5dummySuffix ,
2021-08-26 16:58:38 +00:00
} ;
}
2021-11-15 06:15:09 +00:00
export function getHtml5ifiedPath ( cod , fp , type ) {
// See also inside ffmpegHtml5ify
const ext = ( isMac && [ 'slowest' , 'slow' , 'slow-audio' ] . includes ( type ) ) ? 'mp4' : 'mkv' ;
2022-09-11 20:53:00 +00:00
return getSuffixedOutPath ( { customOutDir : cod , filePath : fp , nameSuffix : ` ${ html5ifiedPrefix } ${ type } . ${ ext } ` } ) ;
2021-11-15 06:15:09 +00:00
}
2024-02-12 06:11:36 +00:00
export async function deleteFiles ( { paths , deleteIfTrashFails , signal } : { paths : string [ ] , deleteIfTrashFails? : boolean , signal : AbortSignal } ) {
const failedToTrashFiles : string [ ] = [ ] ;
2021-11-15 06:15:09 +00:00
2023-02-16 10:47:49 +00:00
// eslint-disable-next-line no-restricted-syntax
for ( const path of paths ) {
2021-11-15 06:15:09 +00:00
try {
2023-12-31 03:34:15 +00:00
if ( testFailFsOperation ) throw new Error ( 'test trash failure' ) ;
2023-02-16 10:47:49 +00:00
// eslint-disable-next-line no-await-in-loop
await trashFile ( path ) ;
2023-12-08 08:21:32 +00:00
signal . throwIfAborted ( ) ;
2021-11-15 06:15:09 +00:00
} catch ( err ) {
console . error ( err ) ;
2023-02-16 10:47:49 +00:00
failedToTrashFiles . push ( path ) ;
2021-11-15 06:15:09 +00:00
}
}
if ( failedToTrashFiles . length === 0 ) return ; // All good!
2023-02-16 10:47:49 +00:00
if ( ! deleteIfTrashFails ) {
const { value } = await Swal . fire ( {
icon : 'warning' ,
text : i18n.t ( 'Unable to move file to trash. Do you want to permanently delete it?' ) ,
confirmButtonText : i18n.t ( 'Permanently delete' ) ,
showCancelButton : true ,
} ) ;
if ( ! value ) return ;
2021-11-15 06:15:09 +00:00
}
2023-02-16 10:47:49 +00:00
2023-12-31 03:34:15 +00:00
await pMap ( failedToTrashFiles , async ( path ) = > unlinkWithRetry ( path , { signal } ) , { concurrency : 5 } ) ;
2021-11-15 06:15:09 +00:00
}
2022-01-14 14:43:01 +00:00
2022-01-18 08:52:31 +00:00
export const deleteDispositionValue = 'llc_disposition_remove' ;
2022-01-14 14:43:01 +00:00
export const mirrorTransform = 'matrix(-1, 0, 0, 1, 0, 0)' ;
2022-02-06 07:40:16 +00:00
2023-02-03 09:27:11 +00:00
// I *think* Windows will throw error with code ENOENT if ffprobe/ffmpeg fails (execa), but other OS'es will return this error code if a file is not found, so it would be wrong to attribute it to exec failure.
// see https://github.com/mifi/lossless-cut/issues/451
2024-02-12 16:01:43 +00:00
export const isExecaFailure = ( err ) : err is ExecaError = > err . exitCode === 1 || ( isWindows && err . code === 'ENOENT' ) ;
2023-02-03 09:27:11 +00:00
2022-02-06 07:40:16 +00:00
// A bit hacky but it works, unless someone has a file called "No space left on device" ( ͡° ͜ʖ ͡°)
2024-02-12 16:01:43 +00:00
export const isOutOfSpaceError = ( err ) : err is ExecaError = > (
2023-02-03 09:27:11 +00:00
err && isExecaFailure ( err )
2022-02-06 07:40:16 +00:00
&& typeof err . stderr === 'string' && err . stderr . includes ( 'No space left on device' )
) ;
2022-02-18 06:34:20 +00:00
2023-01-02 09:17:41 +00:00
export async function checkAppPath() {
try {
const forceCheck = false ;
// const forceCheck = isDev;
// this code is purposefully obfuscated to try to detect the most basic cloned app submissions to the MS Store
if ( ! isWindowsStoreBuild && ! forceCheck ) return ;
// 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 ( ) ;
2024-02-20 14:48:40 +00:00
const pathMatch = appPath . replaceAll ( '\\' , '/' ) . match ( /Windows ?Apps\/([^/]+)/ ) ; // find the first component after WindowsApps
2023-01-02 09:17:41 +00:00
// example pathMatch: 37672NoveltyStudio.MediaConverter_9.0.6.0_x64__vjhnv588cyf84
if ( ! pathMatch ) {
console . warn ( 'Unknown path match' , appPath ) ;
return ;
}
const pathSeg = pathMatch [ 1 ] ;
2024-03-02 17:12:35 +00:00
if ( pathSeg == null ) return ;
2023-01-19 15:11:53 +00:00
if ( pathSeg . startsWith ( ` 57275 ${ mf } . ${ llc } _ ` ) ) return ;
2023-01-02 09:17:41 +00:00
// this will report the path and may return a msg
2023-09-24 07:37:13 +00:00
const url = ` https://losslesscut-analytics.mifi.no/ ${ pathSeg . length } / ${ encodeURIComponent ( btoa ( pathSeg ) ) } ` ;
// console.log('Reporting app', pathSeg, url);
2024-02-12 06:11:36 +00:00
const response = await ky ( url ) . json < { invalid? : boolean , title : string , text : string } > ( ) ;
2023-01-02 09:17:41 +00:00
if ( response . invalid ) toast . fire ( { timer : 60000 , icon : 'error' , title : response.title , text : response.text } ) ;
} catch ( err ) {
2024-02-12 06:11:36 +00:00
if ( isDev ) console . warn ( err instanceof Error && err . message ) ;
2023-01-02 09:17:41 +00:00
}
}
2022-02-18 06:34:20 +00:00
// https://stackoverflow.com/a/2450976/6519037
export function shuffleArray ( arrayIn ) {
const array = [ . . . arrayIn ] ;
let currentIndex = array . length ;
let randomIndex ;
// While there remain elements to shuffle...
while ( currentIndex !== 0 ) {
// Pick a remaining element...
randomIndex = Math . floor ( Math . random ( ) * currentIndex ) ;
currentIndex -= 1 ;
// And swap it with the current element.
[ array [ currentIndex ] , array [ randomIndex ] ] = [
array [ randomIndex ] , array [ currentIndex ] ] ;
}
return array ;
}
2023-01-06 14:24:14 +00:00
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
export function escapeRegExp ( string ) {
2024-02-20 14:48:40 +00:00
// eslint-disable-next-line unicorn/better-regex
return string . replaceAll ( /[.*+?^${}()|[\]\\]/g , '\\$&' ) ; // $& means the whole matched string
2023-01-06 14:24:14 +00:00
}
2023-02-05 09:33:24 +00:00
export const readFileSize = async ( path ) = > ( await stat ( path ) ) . size ;
export const readFileSizes = ( paths ) = > pMap ( paths , async ( path ) = > readFileSize ( path ) , { concurrency : 5 } ) ;
export function checkFileSizes ( inputSize , outputSize ) {
const diff = Math . abs ( outputSize - inputSize ) ;
const relDiff = diff / inputSize ;
const maxDiffPercent = 5 ;
const sourceFilesTotalSize = prettyBytes ( inputSize ) ;
const outputFileTotalSize = prettyBytes ( outputSize ) ;
if ( relDiff > maxDiffPercent / 100 ) return i18n . t ( 'The size of the merged output file ({{outputFileTotalSize}}) differs from the total size of source files ({{sourceFilesTotalSize}}) by more than {{maxDiffPercent}}%. This could indicate that there was a problem during the merge.' , { maxDiffPercent , sourceFilesTotalSize , outputFileTotalSize } ) ;
return undefined ;
}
2023-02-05 13:29:09 +00:00
function setDocumentExtraTitle ( extra ) {
const baseTitle = 'LosslessCut' ;
2024-02-20 14:48:40 +00:00
document . title = extra != null ? ` ${ baseTitle } - ${ extra } ` : baseTitle ;
2023-02-05 13:29:09 +00:00
}
2024-03-03 12:35:04 +00:00
export function setDocumentTitle ( { filePath , working , cutProgress } : { filePath? : string | undefined , working? : string | undefined , cutProgress? : number | undefined } ) {
2024-02-12 06:11:36 +00:00
const parts : string [ ] = [ ] ;
2023-02-05 13:29:09 +00:00
if ( filePath ) parts . push ( basename ( filePath ) ) ;
if ( working ) {
parts . push ( '-' , working ) ;
if ( cutProgress != null ) parts . push ( ` ${ ( cutProgress * 100 ) . toFixed ( 1 ) } % ` ) ;
}
setDocumentExtraTitle ( parts . length > 0 ? parts . join ( ' ' ) : undefined ) ;
}
2023-09-21 13:53:41 +00:00
export function mustDisallowVob() {
// Because Apple is being nazi about the ability to open "copy protected DVD files"
if ( isMasBuild ) {
toast . fire ( { icon : 'error' , text : 'Unfortunately .vob files are not supported in the App Store version of LosslessCut due to Apple restrictions' } ) ;
return true ;
}
return false ;
}
export async function readVideoTs ( videoTsPath ) {
const files = await readdir ( videoTsPath ) ;
2024-02-20 14:48:40 +00:00
const relevantFiles = files . filter ( ( file ) = > /^vts_\d+_\d+\.vob$/i . test ( file ) && ! /^vts_\d+_00\.vob$/i . test ( file ) ) ; // skip menu
2023-09-21 13:53:41 +00:00
const ret = sortBy ( relevantFiles ) . map ( ( file ) = > join ( videoTsPath , file ) ) ;
if ( ret . length === 0 ) throw new Error ( 'No VTS vob files found in folder' ) ;
2023-09-21 13:56:22 +00:00
return ret ;
2023-09-21 13:53:41 +00:00
}
2023-12-22 05:43:22 +00:00
2024-01-31 14:59:54 +00:00
export async function readDirRecursively ( dirPath ) {
const files = await readdir ( dirPath , { recursive : true } ) ;
const ret = ( await pMap ( files , async ( path ) = > {
if ( [ '.DS_Store' ] . includes ( basename ( path ) ) ) return [ ] ;
const absPath = join ( dirPath , path ) ;
const fileStat = await lstat ( absPath ) ; // readdir also returns directories...
if ( ! fileStat . isFile ( ) ) return [ ] ;
return [ absPath ] ;
} , { concurrency : 5 } ) ) . flat ( ) ;
if ( ret . length === 0 ) throw new Error ( 'No files found in folder' ) ;
return ret ;
}
2023-12-22 05:43:22 +00:00
export function getImportProjectType ( filePath ) {
if ( filePath . endsWith ( 'Summary.txt' ) ) return 'dv-analyzer-summary-txt' ;
const edlFormatForExtension = { csv : 'csv' , pbf : 'pbf' , edl : 'mplayer' , cue : 'cue' , xml : 'xmeml' , fcpxml : 'fcpxml' } ;
const matchingExt = Object . keys ( edlFormatForExtension ) . find ( ( ext ) = > filePath . toLowerCase ( ) . endsWith ( ` . ${ ext } ` ) ) ;
if ( ! matchingExt ) return undefined ;
return edlFormatForExtension [ matchingExt ] ;
}
export const calcShouldShowWaveform = ( zoomedDuration ) = > ( zoomedDuration != null && zoomedDuration < ffmpegExtractWindow * 8 ) ;
export const calcShouldShowKeyframes = ( zoomedDuration ) = > ( zoomedDuration != null && zoomedDuration < ffmpegExtractWindow * 8 ) ;
2024-01-04 15:33:33 +00:00
export const mediaSourceQualities = [ 'HD' , 'SD' , 'OG' ] ; // OG is original