Feat: refactor ffmpeg config (#296)
* bundle ffmpeg wasm in package * remove ffmpeg discover & add ffmpeg-static * fix ffprobe-static version * fix metadata generate * fix auto transcribe if pending * remove console.log
This commit is contained in:
16
enjoy/assets/libs/ffmpeg-core.js
Normal file
16
enjoy/assets/libs/ffmpeg-core.js
Normal file
File diff suppressed because one or more lines are too long
BIN
enjoy/assets/libs/ffmpeg-core.wasm
Normal file
BIN
enjoy/assets/libs/ffmpeg-core.wasm
Normal file
Binary file not shown.
1
enjoy/assets/libs/ffmpeg-core.worker.js
Normal file
1
enjoy/assets/libs/ffmpeg-core.worker.js
Normal file
@@ -0,0 +1 @@
|
||||
"use strict";var Module={};var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";if(ENVIRONMENT_IS_NODE){var nodeWorkerThreads=require("worker_threads");var parentPort=nodeWorkerThreads.parentPort;parentPort.on("message",data=>onmessage({data:data}));var fs=require("fs");Object.assign(global,{self:global,require:require,Module:Module,location:{href:__filename},Worker:nodeWorkerThreads.Worker,importScripts:function(f){(0,eval)(fs.readFileSync(f,"utf8")+"//# sourceURL="+f)},postMessage:function(msg){parentPort.postMessage(msg)},performance:global.performance||{now:function(){return Date.now()}}})}var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");if(ENVIRONMENT_IS_NODE){fs.writeSync(2,text+"\n");return}console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=(info,receiveInstance)=>{var module=Module["wasmModule"];Module["wasmModule"]=null;var instance=new WebAssembly.Instance(module,info);return receiveInstance(instance)};self.onunhandledrejection=e=>{throw e.reason??e};function handleMessage(e){try{if(e.data.cmd==="load"){let messageQueue=[];self.onmessage=e=>messageQueue.push(e);self.startWorker=instance=>{Module=instance;postMessage({"cmd":"loaded"});for(let msg of messageQueue){handleMessage(msg)}self.onmessage=handleMessage};Module["wasmModule"]=e.data.wasmModule;for(const handler of e.data.handlers){Module[handler]=function(){postMessage({cmd:"callHandler",handler:handler,args:[...arguments]})}}Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;(e.data.urlOrBlob?import(e.data.urlOrBlob):import("./ffmpeg-core.js")).then(exports=>exports.default(Module))}else if(e.data.cmd==="run"){Module["__emscripten_thread_init"](e.data.pthread_ptr,0,0,1);Module["__emscripten_thread_mailbox_await"](e.data.pthread_ptr);Module["establishStackSpace"]();Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInitTLS();if(!initializedJS){initializedJS=true}try{Module["invokeEntryPoint"](e.data.start_routine,e.data.arg)}catch(ex){if(ex!="unwind"){throw ex}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["__emscripten_thread_exit"](-1)}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="checkMailbox"){if(initializedJS){Module["checkMailbox"]()}}else if(e.data.cmd){err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){if(Module["__emscripten_thread_crashed"]){Module["__emscripten_thread_crashed"]()}throw ex}}self.onmessage=handleMessage;
|
||||
@@ -14,6 +14,8 @@ const NATIVE_MODULES_TO_PACKAGE = [
|
||||
"sqlite3",
|
||||
"fluent-ffmpeg",
|
||||
"electron-squirrel-startup",
|
||||
"ffmpeg-static",
|
||||
"@andrkrn/ffprobe-static",
|
||||
];
|
||||
const INCLUDE_NESTED_DEPS = true as const;
|
||||
let nativeModuleDependenciesToPackage: Set<string>;
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
"@electron-forge/plugin-vite": "^7.2.0",
|
||||
"@electron-forge/publisher-github": "^7.2.0",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/autosize": "^4.0.3",
|
||||
"@types/command-exists": "^1.2.3",
|
||||
"@types/fluent-ffmpeg": "^2.1.24",
|
||||
@@ -64,6 +63,7 @@
|
||||
"zx": "^7.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@andrkrn/ffprobe-static": "^5.2.0",
|
||||
"@ffmpeg/ffmpeg": "^0.12.10",
|
||||
"@ffmpeg/util": "^0.12.1",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
@@ -93,7 +93,6 @@
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@vidstack/react": "^1.10.5",
|
||||
"adm-zip": "^0.5.10",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.6.7",
|
||||
"camelcase": "^8.0.0",
|
||||
@@ -111,6 +110,7 @@
|
||||
"electron-log": "^5.1.1",
|
||||
"electron-settings": "^4.0.2",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"html-to-text": "^9.0.5",
|
||||
|
||||
@@ -180,12 +180,13 @@ export class Audio extends Model<Audio> {
|
||||
|
||||
@BeforeCreate
|
||||
static async setupDefaultAttributes(audio: Audio) {
|
||||
if (!settings.ffmpegConfig().ready) return;
|
||||
|
||||
try {
|
||||
const ffmpeg = new Ffmpeg();
|
||||
const fileMetadata = await ffmpeg.generateMetadata(audio.filePath);
|
||||
audio.metadata = Object.assign(audio.metadata || {}, fileMetadata);
|
||||
audio.metadata = Object.assign(audio.metadata || {}, {
|
||||
...fileMetadata,
|
||||
duration: fileMetadata.format.duration,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("failed to generate metadata", err.message);
|
||||
}
|
||||
|
||||
@@ -148,7 +148,6 @@ export class Video extends Model<Video> {
|
||||
// generate cover and upload
|
||||
async generateCover() {
|
||||
if (this.coverUrl) return;
|
||||
if (!settings.ffmpegConfig().ready) return;
|
||||
|
||||
const ffmpeg = new Ffmpeg();
|
||||
const coverFile = await ffmpeg.generateCover(
|
||||
@@ -202,12 +201,13 @@ export class Video extends Model<Video> {
|
||||
|
||||
@BeforeCreate
|
||||
static async setupDefaultAttributes(video: Video) {
|
||||
if (!settings.ffmpegConfig().ready) return;
|
||||
|
||||
try {
|
||||
const ffmpeg = new Ffmpeg();
|
||||
const fileMetadata = await ffmpeg.generateMetadata(video.filePath);
|
||||
video.metadata = Object.assign(video.metadata || {}, fileMetadata);
|
||||
video.metadata = Object.assign(video.metadata || {}, {
|
||||
...fileMetadata,
|
||||
duration: fileMetadata.format.duration,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("failed to generate metadata", err.message);
|
||||
}
|
||||
|
||||
@@ -1,38 +1,22 @@
|
||||
import { ipcMain } from "electron";
|
||||
import ffmpegPath from "ffmpeg-static";
|
||||
import ffprobePath from "@andrkrn/ffprobe-static";
|
||||
import Ffmpeg from "fluent-ffmpeg";
|
||||
import settings from "@main/settings";
|
||||
import log from "electron-log/main";
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
import AdmZip from "adm-zip";
|
||||
import downloader from "@main/downloader";
|
||||
import storage from "@main/storage";
|
||||
import readdirp from "readdirp";
|
||||
import { t } from "i18next";
|
||||
import uniq from "lodash/uniq";
|
||||
|
||||
const logger = log.scope("ffmpeg");
|
||||
export default class FfmpegWrapper {
|
||||
public ffmpeg: Ffmpeg.FfmpegCommand;
|
||||
public config: any;
|
||||
|
||||
constructor(config?: {
|
||||
ffmpegPath: string;
|
||||
ffprobePath: string;
|
||||
commandExists?: boolean;
|
||||
}) {
|
||||
this.config = config || settings.ffmpegConfig();
|
||||
|
||||
if (this.config.commandExists) {
|
||||
logger.info("Using system ffmpeg");
|
||||
this.ffmpeg = Ffmpeg();
|
||||
} else {
|
||||
logger.info("Using downloaded ffmpeg");
|
||||
const ff = Ffmpeg();
|
||||
ff.setFfmpegPath(this.config.ffmpegPath);
|
||||
ff.setFfprobePath(this.config.ffprobePath);
|
||||
this.ffmpeg = ff;
|
||||
}
|
||||
constructor() {
|
||||
const ff = Ffmpeg();
|
||||
logger.debug("Using ffmpeg path:", ffmpegPath);
|
||||
logger.debug("Using ffprobe path:", ffprobePath);
|
||||
ff.setFfmpegPath(ffmpegPath);
|
||||
ff.setFfprobePath(ffprobePath);
|
||||
this.ffmpeg = ff;
|
||||
}
|
||||
|
||||
checkCommand(): Promise<boolean> {
|
||||
@@ -191,274 +175,10 @@ export default class FfmpegWrapper {
|
||||
|
||||
return this.convertToWav(input, output);
|
||||
}
|
||||
}
|
||||
|
||||
export class FfmpegDownloader {
|
||||
public async download(webContents?: Electron.WebContents) {
|
||||
if (process.platform === "win32") {
|
||||
return await this.downloadForWin32(webContents);
|
||||
} else if (process.platform === "darwin") {
|
||||
return await this.downloadForDarwin(webContents);
|
||||
} else {
|
||||
throw new Error(
|
||||
`You are using ${process.platform}, please install ffmpeg manually`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async downloadForDarwinArm64(webContents?: Electron.WebContents) {
|
||||
const DARWIN_FFMPEG_ARM64_URL = storage.getUrl(
|
||||
"ffmpeg-apple-arm64-build-6.0.zip"
|
||||
);
|
||||
|
||||
fs.ensureDirSync(path.join(settings.libraryPath(), "ffmpeg"));
|
||||
|
||||
const ffmpegZipPath = await downloader.download(DARWIN_FFMPEG_ARM64_URL, {
|
||||
webContents,
|
||||
});
|
||||
const ffmepgZip = new AdmZip(ffmpegZipPath);
|
||||
|
||||
ffmepgZip.extractEntryTo(
|
||||
"ffmpeg/ffmpeg",
|
||||
path.join(settings.libraryPath(), "ffmpeg"),
|
||||
false,
|
||||
true
|
||||
);
|
||||
|
||||
ffmepgZip.extractEntryTo(
|
||||
"ffmpeg/ffprobe",
|
||||
path.join(settings.libraryPath(), "ffmpeg"),
|
||||
false,
|
||||
true
|
||||
);
|
||||
|
||||
fs.chmodSync(path.join(settings.libraryPath(), "ffmpeg", "ffmpeg"), 0o775);
|
||||
fs.chmodSync(path.join(settings.libraryPath(), "ffmpeg", "ffprobe"), 0o775);
|
||||
}
|
||||
|
||||
async downloadForDarwin(webContents?: Electron.WebContents) {
|
||||
if (process.arch === "arm64") {
|
||||
return this.downloadForDarwinArm64(webContents);
|
||||
}
|
||||
|
||||
const DARWIN_FFMPEG_URL = "https://evermeet.cx/ffmpeg/getrelease/zip";
|
||||
const DARWIN_FFPROBE_URL =
|
||||
"https://evermeet.cx/ffmpeg/getrelease/ffprobe/zip";
|
||||
|
||||
fs.ensureDirSync(path.join(settings.libraryPath(), "ffmpeg"));
|
||||
|
||||
const ffmpegZipPath = await downloader.download(DARWIN_FFMPEG_URL, {
|
||||
webContents,
|
||||
});
|
||||
const ffmepgZip = new AdmZip(ffmpegZipPath);
|
||||
ffmepgZip.extractEntryTo(
|
||||
"ffmpeg",
|
||||
path.join(settings.libraryPath(), "ffmpeg"),
|
||||
false,
|
||||
true
|
||||
);
|
||||
|
||||
fs.chmodSync(path.join(settings.libraryPath(), "ffmpeg", "ffmpeg"), 0o775);
|
||||
|
||||
const ffprobeZipPath = await downloader.download(DARWIN_FFPROBE_URL, {
|
||||
webContents,
|
||||
});
|
||||
const ffprobeZip = new AdmZip(ffprobeZipPath);
|
||||
ffprobeZip.extractEntryTo(
|
||||
"ffprobe",
|
||||
path.join(settings.libraryPath(), "ffmpeg"),
|
||||
false,
|
||||
true
|
||||
);
|
||||
fs.chmodSync(path.join(settings.libraryPath(), "ffmpeg", "ffprobe"), 0o775);
|
||||
|
||||
return settings.ffmpegConfig();
|
||||
}
|
||||
|
||||
async downloadForWin32(webContents?: Electron.WebContents) {
|
||||
const WINDOWS_DOWNLOAD_URL =
|
||||
"https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip";
|
||||
|
||||
const zipPath = await downloader.download(WINDOWS_DOWNLOAD_URL, {
|
||||
webContents,
|
||||
});
|
||||
fs.ensureDirSync(path.join(settings.libraryPath(), "ffmpeg"));
|
||||
const zip = new AdmZip(zipPath);
|
||||
|
||||
zip.extractEntryTo(
|
||||
`${path.basename(zipPath, ".zip")}/bin/ffmpeg.exe`,
|
||||
path.join(settings.libraryPath(), "ffmpeg"),
|
||||
false,
|
||||
true
|
||||
);
|
||||
|
||||
zip.extractEntryTo(
|
||||
`${path.basename(zipPath, ".zip")}/bin/ffprobe.exe`,
|
||||
path.join(settings.libraryPath(), "ffmpeg"),
|
||||
false,
|
||||
true
|
||||
);
|
||||
|
||||
return settings.ffmpegConfig();
|
||||
}
|
||||
|
||||
unzip(zipPath: string) {
|
||||
if (!fs.existsSync(zipPath)) {
|
||||
throw new Error(`File ${zipPath} does not exist`);
|
||||
}
|
||||
|
||||
const dir = path.dirname(zipPath);
|
||||
const zip = new AdmZip(zipPath);
|
||||
zip.extractAllTo(dir, true);
|
||||
|
||||
const unzipPath = zipPath.replace(".zip", "");
|
||||
return unzipPath;
|
||||
}
|
||||
|
||||
registerIpcHandlers() {
|
||||
ipcMain.handle("ffmpeg-config", async (_event) => {
|
||||
return settings.ffmpegConfig();
|
||||
});
|
||||
|
||||
ipcMain.handle("ffmpeg-set-config", async (_event, config) => {
|
||||
settings.setSync("ffmpeg.ffmpegPath", config.ffmpegPath);
|
||||
settings.setSync("ffmpeg.ffprobePath", config.ffrobePath);
|
||||
});
|
||||
|
||||
ipcMain.handle("ffmpeg-download", async (event) => {
|
||||
try {
|
||||
return await this.download(event.sender);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
event.sender.send("on-notification", {
|
||||
type: "error",
|
||||
message: `FFmpeg download failed: ${err.message}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("ffmpeg-check-command", async (event) => {
|
||||
const ffmpeg = new FfmpegWrapper();
|
||||
const valid = await ffmpeg.checkCommand();
|
||||
|
||||
if (valid) {
|
||||
event.sender.send("on-notification", {
|
||||
type: "success",
|
||||
message: t("ffmpegCommandIsWorking"),
|
||||
});
|
||||
} else {
|
||||
logger.error("FFmpeg command not valid", ffmpeg.config);
|
||||
event.sender.send("on-notification", {
|
||||
type: "warning",
|
||||
message: t("ffmpegCommandIsNotWorking"),
|
||||
});
|
||||
}
|
||||
|
||||
return valid;
|
||||
});
|
||||
|
||||
ipcMain.handle("ffmpeg-discover-command", async (event) => {
|
||||
try {
|
||||
return await discoverFfmpeg();
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
event.sender.send("on-notification", {
|
||||
type: "error",
|
||||
message: `FFmpeg discover failed: ${err.message}`,
|
||||
});
|
||||
}
|
||||
ipcMain.handle("ffmpeg-check-command", async (_event) => {
|
||||
return await this.checkCommand();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const discoverFfmpeg = async () => {
|
||||
const platform = process.platform;
|
||||
let ffmpegPath: string;
|
||||
let ffprobePath: string;
|
||||
const libraryFfmpegPath = path.join(settings.libraryPath(), "ffmpeg");
|
||||
|
||||
let scanDirs = [...COMMAND_SCAN_DIR[platform], libraryFfmpegPath];
|
||||
|
||||
const currentFfmpegPath = settings.ffmpegConfig().ffmpegPath as string;
|
||||
const currentFfprobePath = settings.ffmpegConfig().ffprobePath as string;
|
||||
|
||||
if (currentFfmpegPath) {
|
||||
scanDirs.push(path.dirname(currentFfmpegPath));
|
||||
}
|
||||
if (currentFfprobePath) {
|
||||
scanDirs.push(path.dirname(currentFfprobePath));
|
||||
}
|
||||
|
||||
scanDirs = uniq(scanDirs);
|
||||
|
||||
await Promise.all(
|
||||
scanDirs.map(async (dir: string) => {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
|
||||
dir = path.resolve(dir);
|
||||
log.info("FFmpeg scanning: " + dir);
|
||||
|
||||
const fileStream = readdirp(dir, {
|
||||
depth: 3,
|
||||
});
|
||||
|
||||
for await (const entry of fileStream) {
|
||||
const appName = entry.basename
|
||||
.replace(".app", "")
|
||||
.replace(".exe", "")
|
||||
.toLowerCase();
|
||||
|
||||
if (appName === "ffmpeg") {
|
||||
logger.info("Found ffmpeg: ", entry.fullPath);
|
||||
ffmpegPath = entry.fullPath;
|
||||
}
|
||||
|
||||
if (appName === "ffprobe") {
|
||||
logger.info("Found ffprobe: ", entry.fullPath);
|
||||
ffprobePath = entry.fullPath;
|
||||
}
|
||||
|
||||
if (ffmpegPath && ffprobePath) break;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let valid = false;
|
||||
if (ffmpegPath && ffprobePath) {
|
||||
const ffmepg = new FfmpegWrapper({ ffmpegPath, ffprobePath });
|
||||
valid = await ffmepg.checkCommand();
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
settings.setSync("ffmpeg", {
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
});
|
||||
} else {
|
||||
ffmpegPath = undefined;
|
||||
ffprobePath = undefined;
|
||||
settings.setSync("ffmpeg", null);
|
||||
}
|
||||
|
||||
return {
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
scanDirs,
|
||||
};
|
||||
};
|
||||
|
||||
export const COMMAND_SCAN_DIR: { [key: string]: string[] } = {
|
||||
darwin: [
|
||||
"/usr/bin",
|
||||
"/usr/local/bin",
|
||||
"/Applications",
|
||||
process.env.HOME + "/Applications",
|
||||
"/opt/homebrew/bin",
|
||||
],
|
||||
linux: ["/usr/bin", "/usr/local/bin", "/snap/bin"],
|
||||
win32: [
|
||||
process.env.SystemDrive + "\\Program Files\\",
|
||||
process.env.SystemDrive + "\\Program Files (x86)\\",
|
||||
process.env.LOCALAPPDATA + "\\Apps\\2.0\\",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -3,14 +3,10 @@ import { LIBRARY_PATH_SUFFIX, DATABASE_NAME } from "@/constants";
|
||||
import { ipcMain, app } from "electron";
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
import os from "os";
|
||||
import commandExists from "command-exists";
|
||||
import log from "electron-log";
|
||||
import * as i18n from "i18next";
|
||||
import mainWin from "@main/window";
|
||||
|
||||
const logger = log.scope("settings");
|
||||
|
||||
const language = () => {
|
||||
const _language = settings.getSync("language");
|
||||
|
||||
@@ -95,27 +91,6 @@ const userDataPath = () => {
|
||||
return userData;
|
||||
};
|
||||
|
||||
const ffmpegConfig = () => {
|
||||
const ffmpegPath = settings.getSync("ffmpeg.ffmpegPath");
|
||||
const ffprobePath = settings.getSync("ffmpeg.ffprobePath");
|
||||
|
||||
const _commandExists =
|
||||
commandExists.sync("ffmpeg") && commandExists.sync("ffprobe");
|
||||
|
||||
const config = {
|
||||
os: os.platform(),
|
||||
arch: os.arch(),
|
||||
commandExists: _commandExists,
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
ready: Boolean(_commandExists || (ffmpegPath && ffprobePath)),
|
||||
};
|
||||
|
||||
logger.info("ffmpeg config", config);
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
export default {
|
||||
registerIpcHandlers: () => {
|
||||
ipcMain.handle("settings-get-library", (_event) => {
|
||||
@@ -182,7 +157,6 @@ export default {
|
||||
userDataPath,
|
||||
dbPath,
|
||||
whisperConfig,
|
||||
ffmpegConfig,
|
||||
language,
|
||||
switchLanguage,
|
||||
...settings,
|
||||
|
||||
@@ -18,7 +18,7 @@ import "@main/i18n";
|
||||
import log from "electron-log/main";
|
||||
import { WEB_API_URL, REPO_URL } from "@/constants";
|
||||
import { AudibleProvider, TedProvider } from "@main/providers";
|
||||
import { FfmpegDownloader } from "@main/ffmpeg";
|
||||
import Ffmpeg from "@main/ffmpeg";
|
||||
import { Waveform } from "./waveform";
|
||||
|
||||
log.initialize({ preload: true });
|
||||
@@ -26,7 +26,7 @@ const logger = log.scope("window");
|
||||
|
||||
const audibleProvider = new AudibleProvider();
|
||||
const tedProvider = new TedProvider();
|
||||
const ffmpegDownloader = new FfmpegDownloader();
|
||||
const ffmpeg = new Ffmpeg();
|
||||
const waveform = new Waveform();
|
||||
|
||||
const main = {
|
||||
@@ -55,8 +55,8 @@ main.init = () => {
|
||||
// Downloader
|
||||
downloader.registerIpcHandlers();
|
||||
|
||||
// FfmpegDownloader
|
||||
ffmpegDownloader.registerIpcHandlers();
|
||||
// ffmpeg
|
||||
ffmpeg.registerIpcHandlers();
|
||||
|
||||
// AudibleProvider
|
||||
audibleProvider.registerIpcHandlers();
|
||||
|
||||
@@ -386,18 +386,6 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
|
||||
},
|
||||
},
|
||||
ffmpeg: {
|
||||
config: () => {
|
||||
return ipcRenderer.invoke("ffmpeg-config");
|
||||
},
|
||||
setConfig: (config: FfmpegConfigType) => {
|
||||
return ipcRenderer.invoke("ffmpeg-set-config", config);
|
||||
},
|
||||
download: () => {
|
||||
return ipcRenderer.invoke("ffmpeg-download");
|
||||
},
|
||||
discover: () => {
|
||||
return ipcRenderer.invoke("ffmpeg-discover-command");
|
||||
},
|
||||
check: () => {
|
||||
return ipcRenderer.invoke("ffmpeg-check-command");
|
||||
},
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
import { t } from "i18next";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { Button, Progress, toast } from "@renderer/components/ui";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import { CheckCircle2Icon, XCircleIcon, LoaderIcon } from "lucide-react";
|
||||
import Markdown from "react-markdown";
|
||||
|
||||
export const FfmpegCheck = () => {
|
||||
const { ffmpegConfig, setFfmegConfig, EnjoyApp } = useContext(
|
||||
AppSettingsProviderContext
|
||||
);
|
||||
const [scanResult, setScanResult] = useState<{
|
||||
ffmpegPath: string;
|
||||
ffprobePath: string;
|
||||
scanDirs: string[];
|
||||
}>();
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const refreshFfmpegConfig = async () => {
|
||||
EnjoyApp.settings.getFfmpegConfig().then((config) => {
|
||||
setFfmegConfig(config);
|
||||
});
|
||||
};
|
||||
|
||||
const discoverFfmpeg = () => {
|
||||
EnjoyApp.ffmpeg.discover().then((config) => {
|
||||
setScanResult(config);
|
||||
if (config.ffmpegPath && config.ffprobePath) {
|
||||
toast.success(
|
||||
t("ffmpegFoundAt", {
|
||||
path: [config.ffmpegPath, config.ffprobePath].join(", "),
|
||||
})
|
||||
);
|
||||
refreshFfmpegConfig();
|
||||
} else {
|
||||
toast.error(t("ffmpegNotFound"));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const downloadFfmpeg = () => {
|
||||
listenToDownloadState();
|
||||
setDownloading(true);
|
||||
EnjoyApp.ffmpeg
|
||||
.download()
|
||||
.then(() => {
|
||||
refreshFfmpegConfig();
|
||||
})
|
||||
.finally(() => {
|
||||
setDownloading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const listenToDownloadState = () => {
|
||||
EnjoyApp.download.onState((_event, downloadState) => {
|
||||
const { received, total } = downloadState;
|
||||
setProgress(Math.round((received * 100) / total));
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return EnjoyApp.download.removeAllListeners();
|
||||
}, [ffmpegConfig?.ready]);
|
||||
|
||||
useEffect(() => {
|
||||
discoverFfmpeg();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-screen-md mx-auto px-6">
|
||||
{ffmpegConfig?.ready ? (
|
||||
<>
|
||||
<div className="flex justify-center items-center mb-8">
|
||||
<img src="./assets/ffmpeg-logo.svg" className="" />
|
||||
</div>
|
||||
<div className="flex justify-center mb-4">
|
||||
<CheckCircle2Icon className="text-green-500 w-10 h-10 mb-4" />
|
||||
</div>
|
||||
<div className="text-center text-sm opacity-70">
|
||||
{t("ffmpegFoundAt", { path: ffmpegConfig.ffmpegPath })}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex justify-center items-center mb-8">
|
||||
<img src="./assets/ffmpeg-logo.svg" className="" />
|
||||
</div>
|
||||
<div className="flex justify-center mb-4">
|
||||
<XCircleIcon className="text-red-500 w-10 h-10" />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<div className="text-center text-sm mb-2">
|
||||
{t("ffmpegNotFound")}
|
||||
</div>
|
||||
|
||||
{scanResult && (
|
||||
<div className="text-center text-xs text-muted-foreground mb-2">
|
||||
{t("tryingToFindValidFFmepgInTheseDirectories", {
|
||||
dirs: scanResult.scanDirs.join(", "),
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center space-x-4 mb-4">
|
||||
<Button onClick={discoverFfmpeg} variant="default">
|
||||
{t("scan")}
|
||||
</Button>
|
||||
|
||||
{ffmpegConfig.os === "win32" && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={downloading}
|
||||
onClick={downloadFfmpeg}
|
||||
>
|
||||
{downloading && <LoaderIcon className="animate-spin mr-2" />}
|
||||
{t("download")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{downloading && (
|
||||
<div className="w-full">
|
||||
<Progress value={progress} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ffmpegConfig.os === "darwin" && (
|
||||
<div className="my-6 select-text prose mx-auto border rounded-lg p-4">
|
||||
<h3 className="text-center">{t("ffmpegInstallSteps")}</h3>
|
||||
<h4>
|
||||
1. {t("install")}{" "}
|
||||
<a
|
||||
className="cursor-pointer text-blue-500 hover:underline"
|
||||
onClick={() => {
|
||||
EnjoyApp.shell.openExternal("https://brew.sh/");
|
||||
}}
|
||||
>
|
||||
Homebrew
|
||||
</a>
|
||||
</h4>
|
||||
<p>{t("runTheFollowingCommandInTerminal")} </p>
|
||||
<pre>
|
||||
<code>
|
||||
/bin/bash -c "$(curl -fsSL
|
||||
https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<h4>2. {t("install")} FFmpeg</h4>
|
||||
|
||||
<p>{t("runTheFollowingCommandInTerminal")} </p>
|
||||
<pre>
|
||||
<code>brew install ffmpeg</code>
|
||||
</pre>
|
||||
|
||||
<h4>3. {t("scan")} FFmpeg</h4>
|
||||
<p>
|
||||
{t("click")}
|
||||
<Button
|
||||
onClick={discoverFfmpeg}
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="mx-2"
|
||||
>
|
||||
{t("scan")}
|
||||
</Button>
|
||||
, {t("willAutomaticallyFindFFmpeg")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -28,8 +28,6 @@ export * from "./whisper-model-options";
|
||||
export * from "./pitch-contour";
|
||||
export * from "./reset-all-button";
|
||||
|
||||
export * from "./ffmpeg-check";
|
||||
|
||||
export * from "./loader-spin";
|
||||
export * from "./no-records-found";
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ export const MediaTranscription = (props: {
|
||||
removeDbListener(fetchSegmentStats);
|
||||
EnjoyApp.whisper.removeProgressListeners();
|
||||
};
|
||||
}, [mediaId, mediaType]);
|
||||
}, [mediaId, mediaType, transcription]);
|
||||
|
||||
useEffect(() => {
|
||||
containerRef.current
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import { t } from "i18next";
|
||||
import { Button, toast } from "@renderer/components/ui";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import { useContext, useState } from "react";
|
||||
import { EditIcon } from "lucide-react";
|
||||
|
||||
export const FfmpegSettings = () => {
|
||||
const { EnjoyApp, setFfmegConfig, ffmpegConfig } = useContext(
|
||||
AppSettingsProviderContext
|
||||
);
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
const refreshFfmpegConfig = async () => {
|
||||
EnjoyApp.ffmpeg.config().then((config) => {
|
||||
setFfmegConfig(config);
|
||||
});
|
||||
};
|
||||
|
||||
const handleChooseFfmpeg = async () => {
|
||||
const filePaths = await EnjoyApp.dialog.showOpenDialog({
|
||||
properties: ["openFile"],
|
||||
});
|
||||
|
||||
const path = filePaths?.[0];
|
||||
if (!path) return;
|
||||
|
||||
if (path.includes("ffmpeg")) {
|
||||
EnjoyApp.ffmpeg.setConfig({
|
||||
...ffmpegConfig,
|
||||
ffmpegPath: path,
|
||||
});
|
||||
refreshFfmpegConfig();
|
||||
} else if (path.includes("ffprobe")) {
|
||||
EnjoyApp.ffmpeg.setConfig({
|
||||
...ffmpegConfig,
|
||||
ffprobePath: path,
|
||||
});
|
||||
refreshFfmpegConfig();
|
||||
} else {
|
||||
toast.error(t("invalidFfmpegPath"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-start justify-between py-4">
|
||||
<div className="">
|
||||
<div className="mb-2">FFmpeg</div>
|
||||
{editing ? (
|
||||
<>
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className=" text-sm text-muted-foreground">
|
||||
<b>ffmpeg</b>: {ffmpegConfig?.ffmpegPath || ""}
|
||||
</span>
|
||||
<Button
|
||||
onClick={handleChooseFfmpeg}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
<EditIcon className="w-4 h-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className=" text-sm text-muted-foreground">
|
||||
<b>ffprobe</b>: {ffmpegConfig?.ffprobePath || ""}
|
||||
</span>
|
||||
<Button
|
||||
onClick={handleChooseFfmpeg}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
<EditIcon className="w-4 h-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{ffmpegConfig.ready ? (
|
||||
<span>{t("ffmpegCommandIsWorking")}</span>
|
||||
) : (
|
||||
<span>{t("ffmpegCommandIsNotWorking")}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="">
|
||||
<div className="flex items-center justify-end space-x-2 mb-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
EnjoyApp.ffmpeg
|
||||
.discover()
|
||||
.then(({ ffmpegPath, ffprobePath }) => {
|
||||
if (ffmpegPath && ffprobePath) {
|
||||
toast.success(
|
||||
t("ffmpegFoundAt", {
|
||||
path: ffmpegPath + ", " + ffprobePath,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
toast.warning(t("ffmpegNotFound"));
|
||||
}
|
||||
refreshFfmpegConfig();
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("scan")}
|
||||
</Button>
|
||||
<Button
|
||||
variant={editing ? "outline" : "secondary"}
|
||||
size="sm"
|
||||
onClick={() => setEditing(!editing)}
|
||||
>
|
||||
{editing ? t("cancel") : t("edit")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -6,7 +6,6 @@ export * from "./default-engine-settings";
|
||||
export * from "./openai-settings";
|
||||
export * from "./language-settings";
|
||||
export * from "./library-settings";
|
||||
export * from "./ffmpeg-settings";
|
||||
export * from "./whisper-settings";
|
||||
export * from "./google-generative-ai-settings";
|
||||
|
||||
|
||||
@@ -9,20 +9,16 @@ import {
|
||||
LanguageSettings,
|
||||
LibrarySettings,
|
||||
WhisperSettings,
|
||||
FfmpegSettings,
|
||||
OpenaiSettings,
|
||||
ProxySettings,
|
||||
GoogleGenerativeAiSettings,
|
||||
ResetSettings,
|
||||
ResetAllSettings,
|
||||
} from "@renderer/components";
|
||||
import { useState, useContext } from "react";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import { useState } from "react";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
|
||||
export const Preferences = () => {
|
||||
const { ffmpegConfig } = useContext(AppSettingsProviderContext);
|
||||
|
||||
const TABS = [
|
||||
{
|
||||
value: "basic",
|
||||
@@ -42,12 +38,6 @@ export const Preferences = () => {
|
||||
<Separator />
|
||||
<GoogleGenerativeAiSettings />
|
||||
<Separator />
|
||||
{ffmpegConfig.ready && (
|
||||
<>
|
||||
<FfmpegSettings />
|
||||
<Separator />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -16,9 +16,7 @@ type AppSettingsProviderState = {
|
||||
login?: (user: UserType) => void;
|
||||
logout?: () => void;
|
||||
setLibraryPath?: (path: string) => Promise<void>;
|
||||
ffmpegConfig?: FfmpegConfigType;
|
||||
ffmpeg?: FFmpeg;
|
||||
setFfmegConfig?: (config: FfmpegConfigType) => void;
|
||||
EnjoyApp?: EnjoyAppType;
|
||||
language?: "en" | "zh-CN";
|
||||
switchLanguage?: (language: "en" | "zh-CN") => void;
|
||||
@@ -46,7 +44,6 @@ export const AppSettingsProvider = ({
|
||||
const [webApi, setWebApi] = useState<Client>(null);
|
||||
const [user, setUser] = useState<UserType | null>(null);
|
||||
const [libraryPath, setLibraryPath] = useState("");
|
||||
const [ffmpegConfig, setFfmegConfig] = useState<FfmpegConfigType>(null);
|
||||
const [ffmpeg, setFfmpeg] = useState<FFmpeg>(null);
|
||||
const [language, setLanguage] = useState<"en" | "zh-CN">();
|
||||
const [proxy, setProxy] = useState<ProxyConfigType>();
|
||||
@@ -58,7 +55,6 @@ export const AppSettingsProvider = ({
|
||||
fetchVersion();
|
||||
fetchUser();
|
||||
fetchLibraryPath();
|
||||
fetchFfmpegConfig();
|
||||
fetchLanguage();
|
||||
loadFfmpegWASM();
|
||||
fetchProxyConfig();
|
||||
@@ -81,7 +77,7 @@ export const AppSettingsProvider = ({
|
||||
}, [user, apiUrl, language]);
|
||||
|
||||
const loadFfmpegWASM = async () => {
|
||||
const baseURL = "https://unpkg.com/@ffmpeg/core-mt@0.12.6/dist/esm";
|
||||
const baseURL = "assets/libs";
|
||||
ffmpegRef.current.on("log", ({ message }) => {
|
||||
console.log(message);
|
||||
});
|
||||
@@ -124,11 +120,6 @@ export const AppSettingsProvider = ({
|
||||
});
|
||||
};
|
||||
|
||||
const fetchFfmpegConfig = async () => {
|
||||
const config = await EnjoyApp.ffmpeg.config();
|
||||
setFfmegConfig(config);
|
||||
};
|
||||
|
||||
const fetchVersion = async () => {
|
||||
const version = EnjoyApp.app.version;
|
||||
setVersion(version);
|
||||
@@ -204,9 +195,7 @@ export const AppSettingsProvider = ({
|
||||
logout,
|
||||
libraryPath,
|
||||
setLibraryPath: setLibraryPathHandler,
|
||||
ffmpegConfig,
|
||||
ffmpeg,
|
||||
setFfmegConfig,
|
||||
proxy,
|
||||
setProxy: setProxyConfigHandler,
|
||||
initialized,
|
||||
|
||||
8
enjoy/src/types/enjoy-app.d.ts
vendored
8
enjoy/src/types/enjoy-app.d.ts
vendored
@@ -227,15 +227,7 @@ type EnjoyAppType = {
|
||||
removeProgressListeners: () => Promise<void>;
|
||||
};
|
||||
ffmpeg: {
|
||||
config: () => Promise<FfmpegConfigType>;
|
||||
setConfig: (config: FfmpegConfigType) => Promise<FfmpegConfigType>;
|
||||
download: () => Promise<FfmpegConfigType>;
|
||||
check: () => Promise<boolean>;
|
||||
discover: () => Promise<{
|
||||
ffmpegPath: string;
|
||||
ffprobePath: string;
|
||||
scanDirs: string[];
|
||||
}>;
|
||||
};
|
||||
download: {
|
||||
onState: (callback: (event, state) => void) => void;
|
||||
|
||||
@@ -24,7 +24,8 @@ export default defineConfig({
|
||||
"fluent-ffmpeg",
|
||||
"bufferutil",
|
||||
"utf-8-validate",
|
||||
// "node-llama-cpp",
|
||||
"ffmpeg-static",
|
||||
"@andrkrn/ffprobe-static",
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -43,12 +44,6 @@ export default defineConfig({
|
||||
}/${os.platform()}/*`,
|
||||
dest: "lib/youtubedr",
|
||||
},
|
||||
{
|
||||
src: `lib/ffmpeg//${
|
||||
process.env.PACKAGE_OS_ARCH || os.arch()
|
||||
}/${os.platform()}/*`,
|
||||
dest: "lib/ffmpeg",
|
||||
},
|
||||
{
|
||||
src: "src/main/db/migrations/*",
|
||||
dest: "migrations",
|
||||
|
||||
102
yarn.lock
102
yarn.lock
@@ -29,6 +29,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@andrkrn/ffprobe-static@npm:^5.2.0":
|
||||
version: 5.2.0
|
||||
resolution: "@andrkrn/ffprobe-static@npm:5.2.0"
|
||||
dependencies:
|
||||
"@derhuerst/http-basic": "npm:^8.2.0"
|
||||
env-paths: "npm:^2.2.0"
|
||||
https-proxy-agent: "npm:^5.0.0"
|
||||
progress: "npm:^2.0.3"
|
||||
checksum: 10c0/fa151b2ef214e43ca126a5ac34b5f7fcc90884a98bf7b0445f59db50872fcc637b199eb4a8835fbad2be53ed91be4b85958972012f2dfb53139658a4b06972cc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@anthropic-ai/sdk@npm:^0.9.1":
|
||||
version: 0.9.1
|
||||
resolution: "@anthropic-ai/sdk@npm:0.9.1"
|
||||
@@ -327,6 +339,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@derhuerst/http-basic@npm:^8.2.0":
|
||||
version: 8.2.4
|
||||
resolution: "@derhuerst/http-basic@npm:8.2.4"
|
||||
dependencies:
|
||||
caseless: "npm:^0.12.0"
|
||||
concat-stream: "npm:^2.0.0"
|
||||
http-response-object: "npm:^3.0.1"
|
||||
parse-cache-control: "npm:^1.0.1"
|
||||
checksum: 10c0/f500c53d8f587ce980f55638d9912ba03652b7483181b90ed41d74fb7eae85c6e05b2f739d97860413886ada9d58d169ec49208237f62c55bdd542f8003f8389
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@electron-forge/cli@npm:^7.2.0":
|
||||
version: 7.2.0
|
||||
resolution: "@electron-forge/cli@npm:7.2.0"
|
||||
@@ -3410,15 +3434,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/adm-zip@npm:^0.5.5":
|
||||
version: 0.5.5
|
||||
resolution: "@types/adm-zip@npm:0.5.5"
|
||||
dependencies:
|
||||
"@types/node": "npm:*"
|
||||
checksum: 10c0/4976dc61e33534ecfb7eee87e8a587db06420a4eb9f34eee9425aece82e9fdd01ddd22677ff6430c582f4d9735c7d714c71adb8f149e3511bc189501b4113631
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/argparse@npm:1.0.38":
|
||||
version: 1.0.38
|
||||
resolution: "@types/argparse@npm:1.0.38"
|
||||
@@ -3731,6 +3746,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^10.0.3":
|
||||
version: 10.17.60
|
||||
resolution: "@types/node@npm:10.17.60"
|
||||
checksum: 10c0/0742294912a6e79786cdee9ed77cff6ee8ff007b55d8e21170fc3e5994ad3a8101fea741898091876f8dc32b0a5ae3d64537b7176799e92da56346028d2cbcd2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^18.11.18, @types/node@npm:^18.16.3":
|
||||
version: 18.19.8
|
||||
resolution: "@types/node@npm:18.19.8"
|
||||
@@ -4130,13 +4152,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"adm-zip@npm:^0.5.10":
|
||||
version: 0.5.10
|
||||
resolution: "adm-zip@npm:0.5.10"
|
||||
checksum: 10c0/1f391a4e02940688b6ca6d4b3ea96cc82a9dbe1596671d7dbc052f9a53ed2efa6ba9ba253f032ea16e70081f22d6ddd1af2d65d6be700853cdee9c2fc925c20e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"agent-base@npm:5":
|
||||
version: 5.1.1
|
||||
resolution: "agent-base@npm:5.1.1"
|
||||
@@ -4896,7 +4911,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"caseless@npm:~0.12.0":
|
||||
"caseless@npm:^0.12.0, caseless@npm:~0.12.0":
|
||||
version: 0.12.0
|
||||
resolution: "caseless@npm:0.12.0"
|
||||
checksum: 10c0/ccf64bcb6c0232cdc5b7bd91ddd06e23a4b541f138336d4725233ac538041fb2f29c2e86c3c4a7a61ef990b665348db23a047060b9414c3a6603e9fa61ad4626
|
||||
@@ -5301,6 +5316,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"concat-stream@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "concat-stream@npm:2.0.0"
|
||||
dependencies:
|
||||
buffer-from: "npm:^1.0.0"
|
||||
inherits: "npm:^2.0.3"
|
||||
readable-stream: "npm:^3.0.2"
|
||||
typedarray: "npm:^0.0.6"
|
||||
checksum: 10c0/29565dd9198fe1d8cf57f6cc71527dbc6ad67e12e4ac9401feb389c53042b2dceedf47034cbe702dfc4fd8df3ae7e6bfeeebe732cc4fa2674e484c13f04c219a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"console-control-strings@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "console-control-strings@npm:1.1.0"
|
||||
@@ -6012,6 +6039,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "enjoy@workspace:enjoy"
|
||||
dependencies:
|
||||
"@andrkrn/ffprobe-static": "npm:^5.2.0"
|
||||
"@electron-forge/cli": "npm:^7.2.0"
|
||||
"@electron-forge/maker-deb": "npm:^7.2.0"
|
||||
"@electron-forge/maker-rpm": "npm:^7.2.0"
|
||||
@@ -6048,7 +6076,6 @@ __metadata:
|
||||
"@radix-ui/react-toggle": "npm:^1.0.3"
|
||||
"@radix-ui/react-tooltip": "npm:^1.0.7"
|
||||
"@tailwindcss/typography": "npm:^0.5.10"
|
||||
"@types/adm-zip": "npm:^0.5.5"
|
||||
"@types/autosize": "npm:^4.0.3"
|
||||
"@types/command-exists": "npm:^1.2.3"
|
||||
"@types/fluent-ffmpeg": "npm:^2.1.24"
|
||||
@@ -6066,7 +6093,6 @@ __metadata:
|
||||
"@uidotdev/usehooks": "npm:^2.4.1"
|
||||
"@vidstack/react": "npm:^1.10.5"
|
||||
"@vitejs/plugin-react": "npm:^4.2.1"
|
||||
adm-zip: "npm:^0.5.10"
|
||||
autoprefixer: "npm:^10.4.17"
|
||||
autosize: "npm:^6.0.1"
|
||||
axios: "npm:^1.6.7"
|
||||
@@ -6089,6 +6115,7 @@ __metadata:
|
||||
eslint: "npm:^8.56.0"
|
||||
eslint-import-resolver-typescript: "npm:^3.6.1"
|
||||
eslint-plugin-import: "npm:^2.29.1"
|
||||
ffmpeg-static: "npm:^5.2.0"
|
||||
flora-colossus: "npm:^2.0.0"
|
||||
fluent-ffmpeg: "npm:^2.1.2"
|
||||
fs-extra: "npm:^11.2.0"
|
||||
@@ -6866,6 +6893,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ffmpeg-static@npm:^5.2.0":
|
||||
version: 5.2.0
|
||||
resolution: "ffmpeg-static@npm:5.2.0"
|
||||
dependencies:
|
||||
"@derhuerst/http-basic": "npm:^8.2.0"
|
||||
env-paths: "npm:^2.2.0"
|
||||
https-proxy-agent: "npm:^5.0.0"
|
||||
progress: "npm:^2.0.3"
|
||||
checksum: 10c0/ac5126fc1391533476232682df593c88f49707878a392cce118576320cc33a1fc33a2d8c918f7d6ac7fa6472fb9d8a67f6c1d3e3842a760644dfcb73f7d10b46
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"file-entry-cache@npm:^6.0.1":
|
||||
version: 6.0.1
|
||||
resolution: "file-entry-cache@npm:6.0.1"
|
||||
@@ -7815,6 +7854,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"http-response-object@npm:^3.0.1":
|
||||
version: 3.0.2
|
||||
resolution: "http-response-object@npm:3.0.2"
|
||||
dependencies:
|
||||
"@types/node": "npm:^10.0.3"
|
||||
checksum: 10c0/f161db99184087798563cb14c48a67eebe9405668a5ed2341faf85d3079a2c00262431df8e0ccbe274dc6415b6729179f12b09f875d13ad33d83401e4b1ed22e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"http2-wrapper@npm:^1.0.0-beta.5.2":
|
||||
version: 1.0.3
|
||||
resolution: "http2-wrapper@npm:1.0.3"
|
||||
@@ -10636,6 +10684,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"parse-cache-control@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "parse-cache-control@npm:1.0.1"
|
||||
checksum: 10c0/330a0d9e3a22a7b0f6e8a973c0b9f51275642ee28544cd0d546420273946d555d20a5c7b49fca24d68d2e698bae0186f0f41f48d62133d3153c32454db05f2df
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"parse-entities@npm:^4.0.0":
|
||||
version: 4.0.1
|
||||
resolution: "parse-entities@npm:4.0.1"
|
||||
@@ -11421,7 +11476,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0":
|
||||
"readable-stream@npm:^3.0.2, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0":
|
||||
version: 3.6.2
|
||||
resolution: "readable-stream@npm:3.6.2"
|
||||
dependencies:
|
||||
@@ -12911,6 +12966,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typedarray@npm:^0.0.6":
|
||||
version: 0.0.6
|
||||
resolution: "typedarray@npm:0.0.6"
|
||||
checksum: 10c0/6005cb31df50eef8b1f3c780eb71a17925f3038a100d82f9406ac2ad1de5eb59f8e6decbdc145b3a1f8e5836e17b0c0002fb698b9fe2516b8f9f9ff602d36412
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@npm:^5.3.3":
|
||||
version: 5.3.3
|
||||
resolution: "typescript@npm:5.3.3"
|
||||
|
||||
Reference in New Issue
Block a user