preliminary work for guest account support
This commit is contained in:
parent
b6779a7d7c
commit
bbc655b3f9
11 changed files with 132 additions and 50 deletions
|
@ -15,9 +15,9 @@ import path from "path";
|
||||||
|
|
||||||
import authRouter from "./routes/auth";
|
import authRouter from "./routes/auth";
|
||||||
import indexRouter from "./routes/index";
|
import indexRouter from "./routes/index";
|
||||||
|
import adduserRouter from "./routes/adduser";
|
||||||
|
|
||||||
import {createUser} from "./db";
|
import {db, createUser} from "./db";
|
||||||
import db from "./db"
|
|
||||||
|
|
||||||
let app = express();
|
let app = express();
|
||||||
let server = http.createServer(app);
|
let server = http.createServer(app);
|
||||||
|
@ -122,6 +122,7 @@ app.use(passport.authenticate("session"));
|
||||||
|
|
||||||
app.use("/", indexRouter);
|
app.use("/", indexRouter);
|
||||||
app.use("/", authRouter);
|
app.use("/", authRouter);
|
||||||
|
app.use("/", adduserRouter);
|
||||||
|
|
||||||
app.use("/uploads", express.static("uploads"));
|
app.use("/uploads", express.static("uploads"));
|
||||||
|
|
||||||
|
|
12
app/db.ts
12
app/db.ts
|
@ -1,13 +1,11 @@
|
||||||
import type {RequestHandler as Middleware} from 'express';
|
import sqlite3 from "sqlite3";
|
||||||
|
import mkdirp from "mkdirp";
|
||||||
const sqlite3 = require("sqlite3");
|
import crypto from "crypto";
|
||||||
const mkdirp = require("mkdirp");
|
|
||||||
const crypto = require("crypto");
|
|
||||||
|
|
||||||
mkdirp.sync("./uploads");
|
mkdirp.sync("./uploads");
|
||||||
mkdirp.sync("./var/db");
|
mkdirp.sync("./var/db");
|
||||||
|
|
||||||
let db = new sqlite3.Database("./var/db/media.db");
|
export const db = new sqlite3.Database("./var/db/media.db");
|
||||||
|
|
||||||
export function createUser(username: string, password: string) {
|
export function createUser(username: string, password: string) {
|
||||||
var salt = crypto.randomBytes(16);
|
var salt = crypto.randomBytes(16);
|
||||||
|
@ -17,5 +15,3 @@ export function createUser(username: string, password: string) {
|
||||||
salt
|
salt
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default db;
|
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
/* background image by Cole Bemis <https://feathericons.com> */
|
/* background image by Cole Bemis <https://feathericons.com> */
|
||||||
.nav .user {
|
.nav .user {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-user'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'%3E%3C/path%3E%3Ccircle cx='12' cy='7' r='4'%3E%3C/circle%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-user'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'%3E%3C/path%3E%3Ccircle cx='12' cy='7' r='4'%3E%3C/circle%3E%3C/svg%3E");
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center left;
|
background-position: center left;
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,15 @@
|
||||||
/* background image by Cole Bemis <https://feathericons.com> */
|
/* background image by Cole Bemis <https://feathericons.com> */
|
||||||
.nav .logout {
|
.nav .logout {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-log-out'%3E%3Cpath d='M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'%3E%3C/path%3E%3Cpolyline points='16 17 21 12 16 7'%3E%3C/polyline%3E%3Cline x1='21' y1='12' x2='9' y2='12'%3E%3C/line%3E%3C/svg%3E%0A");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-log-out'%3E%3Cpath d='M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'%3E%3C/path%3E%3Cpolyline points='16 17 21 12 16 7'%3E%3C/polyline%3E%3Cline x1='21' y1='12' x2='9' y2='12'%3E%3C/line%3E%3C/svg%3E%0A");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* background image by Cole Bemis <https://feathericons.com> */
|
||||||
|
.nav .adduser {
|
||||||
|
padding-left: 20px;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-user-plus'%3E%3Cpath d='M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2'%3E%3C/path%3E%3Ccircle cx='8.5' cy='7' r='4'%3E%3C/circle%3E%3Cline x1='20' y1='8' x2='20' y2='14'%3E%3C/line%3E%3Cline x1='23' y1='11' x2='17' y2='11'%3E%3C/line%3E%3C/svg%3E");
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center left;
|
background-position: center left;
|
||||||
}
|
}
|
||||||
|
|
|
@ -131,8 +131,6 @@ function uploadFile(file) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (file == null || file == undefined) {
|
if (file == null || file == undefined) {
|
||||||
//file = reader.readAsDataURL(document.getElementById("fileupload").files[0]);
|
|
||||||
//file = reader.readAsDataURL(document.querySelector("#fileupload").files[0]);
|
|
||||||
file = document.querySelector("#fileupload").files[0];
|
file = document.querySelector("#fileupload").files[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
30
app/routes/adduser.ts
Normal file
30
app/routes/adduser.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import type {RequestHandler as Middleware, Router, Request, Response, NextFunction} from 'express';
|
||||||
|
import express from "express";
|
||||||
|
|
||||||
|
import {db, createUser} from "../db";
|
||||||
|
|
||||||
|
const router: Router = express.Router();
|
||||||
|
|
||||||
|
const adminCheck: Middleware = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
//@ts-ignore
|
||||||
|
if (!req.user)
|
||||||
|
return res.status(403).send("You are not authorized to perform this action");
|
||||||
|
else {
|
||||||
|
//@ts-ignore
|
||||||
|
if (req.user.username != "admin")
|
||||||
|
return res.status(403).send("You are not authorized to perform this action");
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get("/adduser", adminCheck, (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
res.locals.filter = null;
|
||||||
|
res.render("adduser", { user: req.user });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/adduser", adminCheck, (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
createUser(req.body.username, req.body.password);
|
||||||
|
res.redirect('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
|
@ -1,13 +1,11 @@
|
||||||
import type {MediaRow, UserRow} from '../types';
|
import type {UserRow} from '../types';
|
||||||
import type {RequestHandler as Middleware} from 'express';
|
|
||||||
|
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import passport from "passport";
|
import passport from "passport";
|
||||||
|
import {Strategy as LocalStrategy} from "passport-local";
|
||||||
|
|
||||||
import { Strategy as LocalStrategy } from "passport-local";
|
import {db} from "../db";
|
||||||
|
|
||||||
import db from "../db";
|
|
||||||
|
|
||||||
let router = express.Router();
|
let router = express.Router();
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import type {RequestHandler as Middleware, Router, Request, Response} from 'express';
|
import type {RequestHandler as Middleware, Router, Request, Response, NextFunction} from 'express';
|
||||||
import types from 'multer';
|
|
||||||
|
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
@ -14,8 +13,8 @@ ffmpeg.setFfprobePath(ffprobepath.path);
|
||||||
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
|
||||||
import db from "../db";
|
import {db, createUser} from "../db";
|
||||||
import {checkAuth, convert, handleUpload} from "./middleware";
|
import {checkAuth, checkSharexAuth, createEmbedData, handleUpload} from "./middleware";
|
||||||
import { MediaRow } from '../types';
|
import { MediaRow } from '../types';
|
||||||
|
|
||||||
function extension(str: String){
|
function extension(str: String){
|
||||||
|
@ -89,13 +88,12 @@ const fetchMedia: Middleware = (req, res, next) => {
|
||||||
|
|
||||||
let router = express.Router();
|
let router = express.Router();
|
||||||
|
|
||||||
router.get("/", (req, res, next) => {
|
router.get("/", (req: Request, res: Response, next: NextFunction) => {
|
||||||
// @ts-ignore, user is part of req header
|
if (!req.user)
|
||||||
if (!req.user) { return res.render("home"); }
|
return res.render("home")
|
||||||
next();
|
next();
|
||||||
}, fetchMedia, (req, res) => {
|
}, fetchMedia, (req: Request, res: Response) => {
|
||||||
res.locals.filter = null;
|
res.locals.filter = null;
|
||||||
// @ts-ignore, user is part of req header
|
|
||||||
res.render("index", { user: req.user });
|
res.render("index", { user: req.user });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -103,10 +101,10 @@ router.get("/gifv/:file", async (req, res, next) => {
|
||||||
let url = `${req.protocol}://${req.get("host")}/uploads/${req.params.file}`;
|
let url = `${req.protocol}://${req.get("host")}/uploads/${req.params.file}`;
|
||||||
let width; let height;
|
let width; let height;
|
||||||
|
|
||||||
let nameAndExtension = extension("uploads/" + req.params.file);
|
let nameAndExtension = extension(`uploads/${req.params.file}`);
|
||||||
if (nameAndExtension[1] == ".mp4" || nameAndExtension[1] == ".mov" || nameAndExtension[1] == ".webm") {
|
if (nameAndExtension[1] == ".mp4" || nameAndExtension[1] == ".mov" || nameAndExtension[1] == ".webm") {
|
||||||
ffmpeg()
|
ffmpeg()
|
||||||
.input("uploads/" + req.params.file)
|
.input(`uploads/${req.params.file}`)
|
||||||
.inputFormat(nameAndExtension[1].substring(1))
|
.inputFormat(nameAndExtension[1].substring(1))
|
||||||
.ffprobe((err: Error, data: ffmpeg.FfprobeData) => {
|
.ffprobe((err: Error, data: ffmpeg.FfprobeData) => {
|
||||||
if (err) return next(err);
|
if (err) return next(err);
|
||||||
|
@ -116,7 +114,7 @@ router.get("/gifv/:file", async (req, res, next) => {
|
||||||
});
|
});
|
||||||
} else if (nameAndExtension[1] == ".gif") {
|
} else if (nameAndExtension[1] == ".gif") {
|
||||||
ffmpeg()
|
ffmpeg()
|
||||||
.input("uploads/" + req.params.file)
|
.input(`uploads/${req.params.file}`)
|
||||||
.inputFormat("gif")
|
.inputFormat("gif")
|
||||||
.ffprobe((err: Error, data: ffmpeg.FfprobeData) => {
|
.ffprobe((err: Error, data: ffmpeg.FfprobeData) => {
|
||||||
if (err) return next(err);
|
if (err) return next(err);
|
||||||
|
@ -130,16 +128,16 @@ router.get("/gifv/:file", async (req, res, next) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/", [upload.array("fileupload"), convert, handleUpload], (req: Request, res: Response) => {
|
router.post("/", [checkAuth, upload.array("fileupload"), createEmbedData, handleUpload], (req: Request, res: Response) => {
|
||||||
return res.redirect("/");
|
res.redirect("/")
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/sharex", [checkAuth, upload.array("fileupload"), convert, handleUpload], (req: Request, res: Response) => {
|
router.post("/sharex", [checkSharexAuth, upload.array("fileupload"), createEmbedData, handleUpload], (req: Request, res: Response) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return res.send(`${req.protocol}://${req.get("host")}/uploads/${req.files[0].filename}`);
|
return res.send(`${req.protocol}://${req.get("host")}/uploads/${req.files[0].filename}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/:id(\\d+)/delete", (req, res, next) => {
|
router.post("/:id(\\d+)/delete", [checkAuth], (req: Request, res: Response, next: NextFunction) => {
|
||||||
db.all("SELECT path FROM media WHERE id = ?", [ req.params.id ], (err: Error, path: Array<any>) => {
|
db.all("SELECT path FROM media WHERE id = ?", [ req.params.id ], (err: Error, path: Array<any>) => {
|
||||||
if (err) { return next(err); }
|
if (err) { return next(err); }
|
||||||
fs.unlink(`uploads/${path[0].path}`, (err => {
|
fs.unlink(`uploads/${path[0].path}`, (err => {
|
||||||
|
|
|
@ -10,15 +10,21 @@ ffmpeg.setFfprobePath(ffprobepath.path);
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import process from "process";
|
import process from "process";
|
||||||
|
|
||||||
import db from "../db";
|
import {db} from "../db";
|
||||||
|
|
||||||
function extension(str: String){
|
function extension(str: String){
|
||||||
let file = str.split("/").pop();
|
let file = str.split("/").pop();
|
||||||
return [file.substr(0,file.lastIndexOf(".")),file.substr(file.lastIndexOf("."),file.length).toLowerCase()];
|
return [file.substr(0,file.lastIndexOf(".")),file.substr(file.lastIndexOf("."),file.length).toLowerCase()];
|
||||||
}
|
}
|
||||||
|
|
||||||
//Checks ShareX key
|
export const checkAuth: Middleware = (req, res, next) => {
|
||||||
export const checkAuth: Middleware = (req: Request, res: Response, next: Function) => {
|
if (!req.user) {
|
||||||
|
return res.status(401);
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkSharexAuth: Middleware = (req, res, next) => {
|
||||||
let auth = process.env.EBAPI_KEY || process.env.EBPASS || "pleaseSetAPI_KEY";
|
let auth = process.env.EBAPI_KEY || process.env.EBPASS || "pleaseSetAPI_KEY";
|
||||||
let key = null;
|
let key = null;
|
||||||
|
|
||||||
|
@ -38,8 +44,8 @@ export const checkAuth: Middleware = (req: Request, res: Response, next: Functio
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
//Converts mp4 to gif and vice versa with ffmpeg
|
//createEmbedDatas mp4 to gif and vice versa with ffmpeg
|
||||||
export const convert: Middleware = (req: Request, res: Response, next: Function) => {
|
export const createEmbedData: Middleware = (req, res, next) => {
|
||||||
for (let file in req.files) {
|
for (let file in req.files) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
let nameAndExtension = extension(req.files[file].originalname);
|
let nameAndExtension = extension(req.files[file].originalname);
|
||||||
|
@ -58,13 +64,20 @@ export const convert: Middleware = (req: Request, res: Response, next: Function)
|
||||||
if (err) return next(err);
|
if (err) return next(err);
|
||||||
console.log(`oembed file created ${nameAndExtension[0]}${nameAndExtension[1]}.json`);
|
console.log(`oembed file created ${nameAndExtension[0]}${nameAndExtension[1]}.json`);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
/**if (nameAndExtension[1] == ".mp4") {
|
export const convert: Middleware = (req, res, next) => {
|
||||||
|
for (let file in req.files) {
|
||||||
|
// @ts-ignore
|
||||||
|
let nameAndExtension = extension(req.files[file].originalname);
|
||||||
|
if (nameAndExtension[1] == ".mp4" || nameAndExtension[1] == ".webm" || nameAndExtension[1] == ".mkv" || nameAndExtension[1] == ".avi" || nameAndExtension[1] == ".mov") {
|
||||||
console.log("Converting " + nameAndExtension[0] + nameAndExtension[1] + " to gif");
|
console.log("Converting " + nameAndExtension[0] + nameAndExtension[1] + " to gif");
|
||||||
console.log(nameAndExtension[0] + nameAndExtension[1]);
|
console.log(nameAndExtension[0] + nameAndExtension[1]);
|
||||||
ffmpeg()
|
ffmpeg()
|
||||||
.input(`uploads/${nameAndExtension[0]}${nameAndExtension[1]}`)
|
.input(`uploads/${nameAndExtension[0]}${nameAndExtension[1]}`)
|
||||||
.inputFormat("mp4")
|
.inputFormat(nameAndExtension[1].substring(1))
|
||||||
.outputFormat("gif")
|
.outputFormat("gif")
|
||||||
.output(`uploads/${nameAndExtension[0]}.gif`)
|
.output(`uploads/${nameAndExtension[0]}.gif`)
|
||||||
.on("end", function() {
|
.on("end", function() {
|
||||||
|
@ -90,13 +103,11 @@ export const convert: Middleware = (req: Request, res: Response, next: Function)
|
||||||
console.log(`Uploaded to uploads/${nameAndExtension[0]}.mp4`);
|
console.log(`Uploaded to uploads/${nameAndExtension[0]}.mp4`);
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
}**/
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
next();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handleUpload: Middleware = (req: Request, res: Response, next: Function) => {
|
export const handleUpload: Middleware = (req, res, next) => {
|
||||||
if (!req.files || Object.keys(req.files).length === 0) {
|
if (!req.files || Object.keys(req.files).length === 0) {
|
||||||
console.log("No files were uploaded");
|
console.log("No files were uploaded");
|
||||||
return res.status(400).send("No files were uploaded.");
|
return res.status(400).send("No files were uploaded.");
|
||||||
|
|
37
app/views/adduser.ejs
Normal file
37
app/views/adduser.ejs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Embedder</title>
|
||||||
|
<link rel="stylesheet" href="/css/base.css">
|
||||||
|
<link rel="stylesheet" href="/css/index.css">
|
||||||
|
<link rel="stylesheet" href="/css/login.css">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="/site.webmanifest">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section class="prompt">
|
||||||
|
<h3>Embedder</h3>
|
||||||
|
<h1>Add User</h1>
|
||||||
|
<form action="/adduser" method="post">
|
||||||
|
<section>
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input id="username" name="username" type="text" autocomplete="username" required autofocus>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<label for="current-password">Password</label>
|
||||||
|
<input id="current-password" name="password" type="password" autocomplete="current-password" required>
|
||||||
|
</section>
|
||||||
|
<button type="submit">Add User</button>
|
||||||
|
</form>
|
||||||
|
<hr>
|
||||||
|
</section>
|
||||||
|
<footer class="info">
|
||||||
|
<p><a href="https://l.nekomimi.pet/project">Created by Wavering Ana</a></p>
|
||||||
|
<p><a href="https://github.com/WaveringAna/Embedder">Github</a></p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -27,6 +27,11 @@ return string.slice((string.lastIndexOf(".") - 2 >>> 0) + 2);
|
||||||
<button class="logout" type="submit">Sign out</button>
|
<button class="logout" type="submit">Sign out</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
|
<% if (user.name == "admin" || user.username == "admin") { %>
|
||||||
|
<li>
|
||||||
|
<button class="adduser" onclick="location.href='/adduser';">Add user</a></button>
|
||||||
|
</li>
|
||||||
|
<% } %>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<header class="header">
|
<header class="header">
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
"copy-files": "copyfiles -a -u 1 app/public/* app/views/* app/public/**/* app/views/**/* dist/",
|
"copy-files": "copyfiles -a -u 1 app/public/* app/views/* app/public/**/* app/views/**/* dist/",
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"postinstall": "npm run tsc && npm run copy-files",
|
"postinstall": "npm run tsc && npm run copy-files",
|
||||||
"dev": "tsc-node-dev --respawn --pretty --transpile-only index.ts"
|
"build": "npm run clean && npm run copy-files && npm run tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue