From 832189a34653acd7004d4ec9271ad3769b8af879 Mon Sep 17 00:00:00 2001 From: waveringana Date: Sat, 18 Nov 2023 12:53:15 -0500 Subject: [PATCH] linting --- .eslintrc.js | 52 ++++-------- app/lib/ffmpeg.ts | 143 +++++++++++++++++++++------------ app/lib/lib.ts | 44 ++++++++--- app/lib/middleware.ts | 102 ++++++++++++++++-------- app/lib/multer.ts | 84 ++++++++++++-------- app/routes/auth.ts | 92 +++++++++++++--------- app/routes/index.ts | 179 +++++++++++++++++++++++++++++------------- docker/Dockerfile | 2 + tests/ffmpeg.ts | 87 ++++++++++++-------- tests/test.ts | 32 ++++---- 10 files changed, 518 insertions(+), 299 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 9bba566..d77c96b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,39 +1,21 @@ module.exports = { - "env": { - "browser": true, - "es2021": true, - "node": true + env: { + browser: true, + es2021: true, + node: true, }, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended" - ], - "overrides": [ - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" + extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + overrides: [], + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + }, + plugins: ["@typescript-eslint"], + rules: { + indent: ["error", 2, { SwitchCase: 1 }], + "linebreak-style": ["error", "unix"], + quotes: ["error", "double"], + semi: ["error", "always"], }, - "plugins": [ - "@typescript-eslint" - ], - "rules": { - "indent": [ - "error", - 2 - ], - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "error", - "double" - ], - "semi": [ - "error", - "always" - ] - } }; diff --git a/app/lib/ffmpeg.ts b/app/lib/ffmpeg.ts index 1abc684..d65c10f 100644 --- a/app/lib/ffmpeg.ts +++ b/app/lib/ffmpeg.ts @@ -1,13 +1,13 @@ import { extension, videoExtensions, imageExtensions } from "./lib"; -import ffmpeg, { FfprobeData, ffprobe } from 'fluent-ffmpeg'; -import ffmpegInstaller from '@ffmpeg-installer/ffmpeg'; -import ffprobeInstaller from '@ffprobe-installer/ffprobe'; -import which from 'which'; +import ffmpeg, { FfprobeData, ffprobe } from "fluent-ffmpeg"; +import ffmpegInstaller from "@ffmpeg-installer/ffmpeg"; +import ffprobeInstaller from "@ffprobe-installer/ffprobe"; +import which from "which"; /** * Enum to represent different types of video encoding methods. - * + * * @enum {string} * @property {string} CPU - Uses the libx264 codec for CPU-based encoding * @property {string} NVIDIA - Uses the h264_nvenc codec for NVIDIA GPU-based encoding @@ -16,11 +16,11 @@ import which from 'which'; * @property {string} APPLE - Uses the h264_videotoolbox codec for Apple GPU/MediaEngine-based encoding */ export enum EncodingType { - CPU = 'libx264', - NVIDIA = 'h264_nvenc', - AMD = 'h264_amf', - INTEL = 'h264_qsv', - APPLE = 'h264_videotoolbox' + CPU = "libx264", + NVIDIA = "h264_nvenc", + AMD = "h264_amf", + INTEL = "h264_qsv", + APPLE = "h264_videotoolbox", } /** @@ -31,7 +31,7 @@ export let currentEncoding: EncodingType = EncodingType.CPU; /** * Sets the current encoding type. - * + * * @param {EncodingType} type - The encoding type to set. */ export const setEncodingType = (type: EncodingType) => { @@ -40,16 +40,20 @@ export const setEncodingType = (type: EncodingType) => { /** * Returns the path to an executable by checking environment variables, the system path, or a default installer. - * + * * @param {string} envVar - The environment variable to check for the executable's path. * @param {string} executable - The name of the executable to search for in the system path. * @param {Object} installer - An object containing the default installer path. * @param {string} installer.path - The default path to use if the executable is not found in the environment or system path. - * + * * @returns {string} - The path to the executable. * @throws Will throw an error if the executable is not found and the installer path is not available. */ -const getExecutablePath = (envVar: string, executable: string, installer: { path: string }) => { +const getExecutablePath = ( + envVar: string, + executable: string, + installer: { path: string }, +) => { if (process.env[envVar]) { return process.env[envVar]; } @@ -61,8 +65,16 @@ const getExecutablePath = (envVar: string, executable: string, installer: { path } }; -const ffmpegPath = getExecutablePath('EB_FFMPEG_PATH', 'ffmpeg', ffmpegInstaller); -const ffprobePath = getExecutablePath('EB_FFPROBE_PATH', 'ffprobe', ffprobeInstaller); +const ffmpegPath = getExecutablePath( + "EB_FFMPEG_PATH", + "ffmpeg", + ffmpegInstaller, +); +const ffprobePath = getExecutablePath( + "EB_FFPROBE_PATH", + "ffprobe", + ffprobeInstaller, +); console.log(`Using ffmpeg from path: ${ffmpegPath}`); console.log(`Using ffprobe from path: ${ffprobePath}`); @@ -74,10 +86,16 @@ const checkEnvForEncoder = () => { const envEncoder = process.env.EB_ENCODER?.toUpperCase(); if (envEncoder && Object.keys(EncodingType).includes(envEncoder)) { - setEncodingType(EncodingType[envEncoder as keyof typeof EncodingType] as EncodingType); - console.log(`Setting encoding type to ${envEncoder} based on environment variable.`); + setEncodingType( + EncodingType[envEncoder as keyof typeof EncodingType] as EncodingType, + ); + console.log( + `Setting encoding type to ${envEncoder} based on environment variable.`, + ); } else if (envEncoder) { - console.warn(`Invalid encoder value "${envEncoder}" in environment variable, defaulting to CPU.`); + console.warn( + `Invalid encoder value "${envEncoder}" in environment variable, defaulting to CPU.`, + ); } }; @@ -85,12 +103,12 @@ checkEnvForEncoder(); /** * Downscale a video using ffmpeg with various encoding options. - * + * * @param {string} path - The input video file path. * @param {string} filename - The name of the file. * @param {string} extension - The file extension of the file * @returns {Promise} - A promise that resolves when the downscaling is complete, and rejects on error. - * + * * @example * ffmpegDownscale('input.mp4').then(() => { * console.log('Downscaling complete.'); @@ -98,13 +116,21 @@ checkEnvForEncoder(); * console.log(`Error: ${error}`); * }); */ -export const ffmpegDownscale = (path: string, filename: string, extension: string) => { +export const ffmpegDownscale = ( + path: string, + filename: string, + extension: string, +) => { const startTime = Date.now(); const outputOptions = [ - '-vf', 'scale=-2:720', - '-c:v', currentEncoding, - '-c:a', 'copy', - "-pix_fmt", "yuv420p", + "-vf", + "scale=-2:720", + "-c:v", + currentEncoding, + "-c:a", + "copy", + "-pix_fmt", + "yuv420p", ]; return new Promise((resolve, reject) => { @@ -112,23 +138,27 @@ export const ffmpegDownscale = (path: string, filename: string, extension: strin .input(path) .outputOptions(outputOptions) .output(`uploads/720p-${filename}${extension}`) - .on('end', () => { - console.log(`720p copy complete using ${currentEncoding}, took ${Date.now() - startTime}ms to complete`); + .on("end", () => { + console.log( + `720p copy complete using ${currentEncoding}, took ${ + Date.now() - startTime + }ms to complete`, + ); resolve(); }) - .on('error', (e) => reject(new Error(e))) + .on("error", (e) => reject(new Error(e))) .run(); }); -} +}; /** * Convert a video to a gif or vice versa using ffmpeg with various encoding options. - * + * * @param {string} path - The input video file path. * @param {string} filename - The name of the file. * @param {string} extension - The file extension of the file * @returns {Promise} - A promise that resolves when the conversion is complete, and rejects on error. - * + * * @example * ffmpegConvert('input.mp4').then(() => { * console.log('Conversion complete.'); @@ -136,22 +166,31 @@ export const ffmpegDownscale = (path: string, filename: string, extension: strin * console.log(`Error: ${error}`); * }); */ -export const ffmpegConvert = (path: string, filename: string, extension: string) => { +export const ffmpegConvert = ( + path: string, + filename: string, + extension: string, +) => { const startTime = Date.now(); const outputOptions = [ - '-vf', 'scale=-2:720', - '-c:v', currentEncoding, - '-c:a', 'copy', - "-movflags", "+faststart", - "-pix_fmt", "yuv420p", - ] + "-vf", + "scale=-2:720", + "-c:v", + currentEncoding, + "-c:a", + "copy", + "-movflags", + "+faststart", + "-pix_fmt", + "yuv420p", + ]; let outputFormat: string; if (videoExtensions.includes(extension)) { - outputFormat = '.gif'; - } else if (extension == '.gif') { - outputFormat = '.mp4'; + outputFormat = ".gif"; + } else if (extension == ".gif") { + outputFormat = ".mp4"; } else { return new Promise((resolve, reject) => { reject(`Submitted file is neither a video nor a gif: ${path}`); @@ -162,24 +201,30 @@ export const ffmpegConvert = (path: string, filename: string, extension: string) ffmpeg() .input(path) .outputOptions(outputOptions) - .output(`uploads/`) + .output("uploads/") .outputFormat(outputFormat) .output(`uploads/${filename}${outputFormat}`) - .on("end", function() { - console.log(`Conversion complete, took ${Date.now() - startTime} to complete`); + .on("end", function () { + console.log( + `Conversion complete, took ${Date.now() - startTime} to complete`, + ); console.log(`uploads/${filename}${outputFormat}`); resolve(); }) .on("error", (e) => reject(e)) .run(); }); -} +}; -export const ffProbe = (path: string, filename: string, extension: string) => { +export const ffProbe = async ( + path: string, + filename: string, + extension: string, +) => { return new Promise((resolve, reject) => { ffprobe(path, (err, data) => { - if (err) reject (err); + if (err) reject(err); resolve(data); }); }); -} +}; diff --git a/app/lib/lib.ts b/app/lib/lib.ts index 11c4e7b..6dfafb5 100644 --- a/app/lib/lib.ts +++ b/app/lib/lib.ts @@ -2,25 +2,45 @@ declare global { namespace Express { interface User { - id? : number | string, - username: string, - hashed_password?: any, - salt?: any + id?: number | string; + username: string; + hashed_password?: any; + salt?: any; } } } /**Splits a file name into its name and then its extension */ -export function extension(str: string){ +export function extension(str: string) { const 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(), + ]; } /**Type for user data */ export interface User { - id? : number | string, - username: string, - hashed_password?: any, - salt?: any + id?: number | string; + username: string; + hashed_password?: any; + salt?: any; } -export const videoExtensions = [".mp4", ".mov", ".avi", ".flv", ".mkv", ".wmv", ".webm"]; -export const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".tiff", ".webp"]; +export const videoExtensions = [ + ".mp4", + ".mov", + ".avi", + ".flv", + ".mkv", + ".wmv", + ".webm", +]; +export const imageExtensions = [ + ".jpg", + ".jpeg", + ".png", + ".gif", + ".bmp", + ".svg", + ".tiff", + ".webp", +]; diff --git a/app/lib/middleware.ts b/app/lib/middleware.ts index a0b5ebe..49ac254 100644 --- a/app/lib/middleware.ts +++ b/app/lib/middleware.ts @@ -1,11 +1,12 @@ -import type {RequestHandler as Middleware, NextFunction} from "express"; +import type { RequestHandler as Middleware, NextFunction } from "express"; import fs from "fs"; import process from "process"; -import {extension, videoExtensions, imageExtensions} from "./lib"; -import {insertToDB} from "./db"; -import {ffmpegDownscale} from "./ffmpeg"; +import { extension, videoExtensions, imageExtensions } from "./lib"; +import { insertToDB } from "./db"; +import { ffmpegDownscale, ffProbe } from "./ffmpeg"; +import { ffprobe } from "fluent-ffmpeg"; export const checkAuth: Middleware = (req, res, next) => { if (!req.user) { @@ -16,45 +17,70 @@ export const checkAuth: Middleware = (req, res, next) => { /**Checks shareX auth key */ export const checkSharexAuth: Middleware = (req, res, next) => { - const auth = process.env.EBAPI_KEY || process.env.EBPASS || "pleaseSetAPI_KEY"; + const auth = + process.env.EBAPI_KEY || process.env.EBPASS || "pleaseSetAPI_KEY"; let key = null; - + if (req.headers["key"]) { key = req.headers["key"]; } else { - return res.status(400).send("{success: false, message: \"No key provided\", fix: \"Provide a key\"}"); + return res + .status(400) + .send( + "{success: false, message: 'No key provided', fix: 'Provide a key'}", + ); } - + if (auth != key) { - return res.status(401).send("{success: false, message: '\"'Invalid key\", fix: \"Provide a valid key\"}"); + return res + .status(401) + .send( + "{success: false, message: 'Invalid key', fix: 'Provide a valid key'}", + ); } - - const shortKey = key.substr(0, 3) + "..."; + + const shortKey = key.substr(0, 3) + "..."; console.log(`Authenicated user with key: ${shortKey}`); - + next(); }; /**Creates oembed json file for embed metadata */ -export const createEmbedData: Middleware = (req, res, next) => { +export const createEmbedData: Middleware = async (req, res, next) => { const files = req.files as Express.Multer.File[]; for (const file in files) { const nameAndExtension = extension(files[file].originalname); + const ffProbeData = await ffProbe( + `uploads/${files[file].originalname}`, + nameAndExtension[0], + nameAndExtension[1], + ); + const width = ffProbeData.streams[0].width; + const height = ffProbeData.streams[0].height; + const oembed = { type: "video", version: "1.0", provider_name: "embedder", provider_url: "https://github.com/WaveringAna/embedder", cache_age: 86400, - html: ``, - width: 640, - height: 360 + html: ``, + width: width, + height: height, }; - - fs.writeFile(`uploads/oembed-${nameAndExtension[0]}${nameAndExtension[1]}.json`, JSON.stringify(oembed), function (err) { - if (err) return next(err); - console.log(`oembed file created ${nameAndExtension[0]}${nameAndExtension[1]}.json`); - }); + + fs.writeFile( + `uploads/oembed-${nameAndExtension[0]}${nameAndExtension[1]}.json`, + JSON.stringify(oembed), + function (err) { + if (err) return next(err); + console.log( + `oembed file created ${nameAndExtension[0]}${nameAndExtension[1]}.json`, + ); + }, + ); } next(); }; @@ -67,7 +93,10 @@ export const convertTo720p: Middleware = (req, res, next) => { const nameAndExtension = extension(files[file].originalname); //Skip if not a video - if (!videoExtensions.includes(nameAndExtension[1]) && nameAndExtension[1] !== ".gif") { + if ( + !videoExtensions.includes(nameAndExtension[1]) && + nameAndExtension[1] !== ".gif" + ) { console.log(`${files[file].originalname} is not a video file`); console.log(nameAndExtension[1]); continue; @@ -75,11 +104,17 @@ export const convertTo720p: Middleware = (req, res, next) => { console.log(`Creating 720p for ${files[file].originalname}`); - ffmpegDownscale(`uploads/${nameAndExtension[0]}${nameAndExtension[1]}`, nameAndExtension[0], nameAndExtension[1]).then(() => { - //Nothing for now, can fire event flag that it is done to front end when react conversion is done - }).catch((error) => { - console.log(`Error: ${error}`); - }); + ffmpegDownscale( + `uploads/${nameAndExtension[0]}${nameAndExtension[1]}`, + nameAndExtension[0], + nameAndExtension[1], + ) + .then(() => { + //Nothing for now, can fire event flag that it is done to front end when react conversion is done + }) + .catch((error) => { + console.log(`Error: ${error}`); + }); } next(); @@ -91,17 +126,18 @@ export const handleUpload: Middleware = (req, res, next) => { console.log("No files were uploaded"); return res.status(400).send("No files were uploaded."); } - - const files = (req.files) ? req.files as Express.Multer.File[] : req.file; //Check if a single file was uploaded or multiple - const username = (req.user) ? req.user.username : "sharex"; //if no username was provided, we can presume that it is sharex - const expireDate: Date = (req.body.expire) ? new Date(Date.now() + (req.body.expire * 24 * 60 * 60 * 1000)) : null; + + const files = req.files ? (req.files as Express.Multer.File[]) : req.file; //Check if a single file was uploaded or multiple + const username = req.user ? req.user.username : "sharex"; //if no username was provided, we can presume that it is sharex + const expireDate: Date = req.body.expire + ? new Date(Date.now() + req.body.expire * 24 * 60 * 60 * 1000) + : null; if (files instanceof Array) { for (const file in files) { insertToDB(files[file].filename, expireDate, username); } - } else - insertToDB(files.filename, expireDate, username); + } else insertToDB(files.filename, expireDate, username); next(); }; diff --git a/app/lib/multer.ts b/app/lib/multer.ts index 64f4568..77f1e20 100644 --- a/app/lib/multer.ts +++ b/app/lib/multer.ts @@ -1,49 +1,70 @@ -import {Request} from "express"; -import multer, {FileFilterCallback} from "multer"; +import { Request } from "express"; +import multer, { FileFilterCallback } from "multer"; -import {db, MediaRow} from "./db"; -import {extension} from "./lib"; +import { db, MediaRow } from "./db"; +import { extension } from "./lib"; -export type DestinationCallback = (error: Error | null, destination: string) => void -export type FileNameCallback = (error: Error | null, filename: string) => void +export type DestinationCallback = ( + error: Error | null, + destination: string, +) => void; +export type FileNameCallback = (error: Error | null, filename: string) => void; export const fileStorage = multer.diskStorage({ destination: ( request: Request, file: Express.Multer.File, - callback: DestinationCallback + callback: DestinationCallback, ): void => { callback(null, __dirname + "/../../uploads"); }, filename: ( request: Request, file: Express.Multer.File, - callback: FileNameCallback + callback: FileNameCallback, ): void => { const nameAndExtension = extension(file.originalname); console.log(`Uploading ${file}`); - db.all("SELECT * FROM media WHERE path = ?", [nameAndExtension[0] + nameAndExtension[1]], (err: Error, exists: []) => { - if (err) { - console.log(err); - callback(err, null); - } - if (exists.length != 0) { - const suffix = new Date().getTime() / 1000; + db.all( + "SELECT * FROM media WHERE path = ?", + [nameAndExtension[0] + nameAndExtension[1]], + (err: Error, exists: []) => { + if (err) { + console.log(err); + callback(err, null); + } + if (exists.length != 0) { + const suffix = new Date().getTime() / 1000; - if (request.body.title == "" || request.body.title == null || request.body.title == undefined) { - callback(null, nameAndExtension[0] + "-" + suffix + nameAndExtension[1]); + if ( + request.body.title == "" || + request.body.title == null || + request.body.title == undefined + ) { + callback( + null, + nameAndExtension[0] + "-" + suffix + nameAndExtension[1], + ); + } else { + callback( + null, + request.body.title + "-" + suffix + nameAndExtension[1], + ); + } } else { - callback(null, request.body.title + "-" + suffix + nameAndExtension[1]); + if ( + request.body.title == "" || + request.body.title == null || + request.body.title == undefined + ) { + callback(null, nameAndExtension[0] + nameAndExtension[1]); + } else { + callback(null, request.body.title + nameAndExtension[1]); + } } - } else { - if (request.body.title == "" || request.body.title == null || request.body.title == undefined) { - callback(null, nameAndExtension[0] + nameAndExtension[1]); - } else { - callback(null, request.body.title + nameAndExtension[1]); - } - } - }); - } + }, + ); + }, }); export const allowedMimeTypes = [ @@ -56,17 +77,18 @@ export const allowedMimeTypes = [ "video/mov", "video/webm", "audio/mpeg", - "audio/ogg" + "audio/ogg", ]; - + export const fileFilter = ( request: Request, file: Express.Multer.File, - callback: FileFilterCallback + callback: FileFilterCallback, ): void => { if (allowedMimeTypes.includes(file.mimetype)) { callback(null, true); } else { callback(null, false); } -}; \ No newline at end of file +}; + diff --git a/app/routes/auth.ts b/app/routes/auth.ts index 1103a8b..0a3164f 100644 --- a/app/routes/auth.ts +++ b/app/routes/auth.ts @@ -1,64 +1,80 @@ import crypto from "crypto"; import express from "express"; import passport from "passport"; -import {Strategy as LocalStrategy} from "passport-local"; +import { Strategy as LocalStrategy } from "passport-local"; -import {User} from "../lib/lib"; -import {db, UserRow} from "../lib/db"; +import { User } from "../lib/lib"; +import { db, UserRow } from "../lib/db"; const router = express.Router(); -passport.use(new LocalStrategy(function verify(username, password, cb) { - db.get("SELECT * FROM users WHERE username = ?", [username], function(err: Error, row: UserRow) { - if (err) { - return cb(err); - } - if (!row) { - return cb(null, false, { - message: "Incorrect username or password." - }); - } +passport.use( + new LocalStrategy(function verify(username, password, cb) { + db.get( + "SELECT * FROM users WHERE username = ?", + [username], + function (err: Error, row: UserRow) { + if (err) { + return cb(err); + } + if (!row) { + return cb(null, false, { + message: "Incorrect username or password.", + }); + } - crypto.pbkdf2(password, row.salt, 310000, 32, "sha256", function(err, hashedPassword) { - if (err) { - return cb(err); - } - if (!crypto.timingSafeEqual(row.hashed_password, hashedPassword)) { - return cb(null, false, { - message: "Incorrect username or password." - }); - } - return cb(null, row); - }); - }); -})); + crypto.pbkdf2( + password, + row.salt, + 310000, + 32, + "sha256", + function (err, hashedPassword) { + if (err) { + return cb(err); + } + if (!crypto.timingSafeEqual(row.hashed_password, hashedPassword)) { + return cb(null, false, { + message: "Incorrect username or password.", + }); + } + return cb(null, row); + }, + ); + }, + ); + }), +); -passport.serializeUser(function(user:User, cb) { - process.nextTick(function() { +passport.serializeUser(function (user: User, cb) { + process.nextTick(function () { cb(null, { id: user.id, - username: user.username + username: user.username, }); }); }); -passport.deserializeUser(function(user:User, cb) { - process.nextTick(function() { +passport.deserializeUser(function (user: User, cb) { + process.nextTick(function () { return cb(null, user); }); }); -router.get("/login", function(req, res) { +router.get("/login", function (req, res) { res.render("login"); }); -router.post("/login/password", passport.authenticate("local", { - successRedirect: "/", - failureRedirect: "/login" -})); +router.post( + "/login/password", + passport.authenticate("local", { + successRedirect: "/", + failureRedirect: "/login", + }), +); -router.post("/logout", function(req, res, next) { - req.logout(function(err) { +router.post("/logout", function (req, res, next) { + req.logout(function (err) { if (err) { return next(err); } diff --git a/app/routes/index.ts b/app/routes/index.ts index 180d38d..7190464 100644 --- a/app/routes/index.ts +++ b/app/routes/index.ts @@ -1,33 +1,47 @@ -import type {RequestHandler as Middleware, Request, Response, NextFunction} from "express"; +import type { + RequestHandler as Middleware, + Request, + Response, + NextFunction, +} from "express"; import multer from "multer"; import express from "express"; import imageProbe from "probe-image-size"; -import {ffProbe} from "../lib/ffmpeg"; +import { ffProbe } from "../lib/ffmpeg"; import fs from "fs"; -import {extension, videoExtensions} from "../lib/lib"; -import {db, MediaRow, getPath, deleteId} from "../lib/db"; -import {fileStorage} from "../lib/multer"; -import {checkAuth, checkSharexAuth, convertTo720p, createEmbedData, handleUpload} from "../lib/middleware"; +import { extension, videoExtensions } from "../lib/lib"; +import { db, MediaRow, getPath, deleteId } from "../lib/db"; +import { fileStorage } from "../lib/multer"; +import { + checkAuth, + checkSharexAuth, + convertTo720p, + createEmbedData, + handleUpload, +} from "../lib/middleware"; const upload = multer({ storage: fileStorage /**, fileFilter: fileFilter**/ }); //maybe make this a env variable? /**Middleware to grab media from media database */ const fetchMedia: Middleware = (req, res, next) => { const admin: boolean = req.user.username == "admin" ? true : false; /**Check if the user is an admin, if so, show all posts from all users */ - const query: string = admin == true ? "SELECT * FROM media" : `SELECT * FROM media WHERE username = '${req.user.username}'`; + const query: string = + admin == true + ? "SELECT * FROM media" + : `SELECT * FROM media WHERE username = '${req.user.username}'`; - db.all(query, (err:Error, rows: []) => { + db.all(query, (err: Error, rows: []) => { if (err) return next(err); - const files = rows.map((row: MediaRow)=> { + const files = rows.map((row: MediaRow) => { return { id: row.id, path: row.path, expire: row.expire, username: row.username, - url: "/" + row.id + url: "/" + row.id, }; }); res.locals.files = files.reverse(); //reverse so newest files appear first @@ -38,63 +52,118 @@ const fetchMedia: Middleware = (req, res, next) => { const router = express.Router(); -router.get("/", (req: Request, res: Response, next: NextFunction) => { - if (!req.user) - return res.render("home"); - next(); -}, fetchMedia, (req: Request, res: Response) => { - res.locals.filter = null; - res.render("index", { user: req.user }); -}); +router.get( + "/", + (req: Request, res: Response, next: NextFunction) => { + if (!req.user) return res.render("home"); + next(); + }, + fetchMedia, + (req: Request, res: Response) => { + res.locals.filter = null; + res.render("index", { user: req.user }); + }, +); -router.get("/gifv/:file", async (req: Request, res: Response, next: NextFunction) => { - const url = `${req.protocol}://${req.get("host")}/uploads/${req.params.file}`; - let width; let height; +router.get( + "/gifv/:file", + async (req: Request, res: Response, next: NextFunction) => { + const url = `${req.protocol}://${req.get("host")}/uploads/${ + req.params.file + }`; + let width; + let height; - const nameAndExtension = extension(`uploads/${req.params.file}`); - if (nameAndExtension[1] == ".mp4" || nameAndExtension[1] == ".mov" || nameAndExtension[1] == ".webm" || nameAndExtension[1] == ".gif") { - let imageData = ffProbe(`uploads/${req.params.file}`, nameAndExtension[0], nameAndExtension[1]); + const nameAndExtension = extension(`uploads/${req.params.file}`); + if ( + nameAndExtension[1] == ".mp4" || + nameAndExtension[1] == ".mov" || + nameAndExtension[1] == ".webm" || + nameAndExtension[1] == ".gif" + ) { + const imageData = ffProbe( + `uploads/${req.params.file}`, + nameAndExtension[0], + nameAndExtension[1], + ); - width = (await imageData).streams[0].width; - height = (await imageData).streams[0].height; + width = (await imageData).streams[0].width; + height = (await imageData).streams[0].height; - return res.render("gifv", { url: url, host: `${req.protocol}://${req.get("host")}`, width: width, height: height }); - } else { - const imageData = await imageProbe(fs.createReadStream(`uploads/${req.params.file}`)); - return res.render("gifv", { url: url, host: `${req.protocol}://${req.get("host")}`, width: imageData.width, height: imageData.height }); - } -}); + return res.render("gifv", { + url: url, + host: `${req.protocol}://${req.get("host")}`, + width: width, + height: height, + }); + } else { + const imageData = await imageProbe( + fs.createReadStream(`uploads/${req.params.file}`), + ); + return res.render("gifv", { + url: url, + host: `${req.protocol}://${req.get("host")}`, + width: imageData.width, + height: imageData.height, + }); + } + }, +); -router.post("/", [checkAuth, upload.array("fileupload"), convertTo720p, createEmbedData, handleUpload], (req: Request, res: Response) => { - res.redirect("/"); -}); +router.post( + "/", + [ + checkAuth, + upload.array("fileupload"), + convertTo720p, + createEmbedData, + handleUpload, + ], + (req: Request, res: Response) => { + res.redirect("/"); + }, +); -router.post("/sharex", [checkSharexAuth, upload.single("fileupload"), createEmbedData, handleUpload], (req: Request, res: Response) => { - return res.send(`${req.protocol}://${req.get("host")}/uploads/${req.file.filename}`); -}); +router.post( + "/sharex", + [checkSharexAuth, upload.single("fileupload"), createEmbedData, handleUpload], + (req: Request, res: Response) => { + return res.send( + `${req.protocol}://${req.get("host")}/uploads/${req.file.filename}`, + ); + }, +); -router.post("/:id(\\d+)/delete", [checkAuth], async (req: Request, res: Response) => { - const path: any = await getPath(req.params.id); +router.post( + "/:id(\\d+)/delete", + [checkAuth], + async (req: Request, res: Response) => { + const path: any = await getPath(req.params.id); - const nameAndExtension = extension(path.path); + const nameAndExtension = extension(path.path); - const filesToDelete = [path.path, "oembed-" + path.path + ".json"]; + const filesToDelete = [path.path, "oembed-" + path.path + ".json"]; - if (videoExtensions.includes(nameAndExtension[1]) || nameAndExtension[1] == ".gif") { - filesToDelete.push("720p-" + path.path); - } + if ( + videoExtensions.includes(nameAndExtension[1]) || + nameAndExtension[1] == ".gif" + ) { + filesToDelete.push("720p-" + path.path); + } - filesToDelete.forEach(path => { - fs.unlink(path, async (err) => { - console.log(`Deleting ${path}`); - if (err && err.errno == -4058) { + filesToDelete.forEach((path) => { + fs.unlink(path, async (err) => { + console.log(`Deleting ${path}`); + if (err && err.errno == -4058) { + await deleteId("media", req.params.id); + } await deleteId("media", req.params.id); - } - await deleteId("media", req.params.id); + }); }); - }); - return res.redirect("/"); -}); + return res.redirect("/"); + }, +); + +export default router; -export default router; \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 6be7234..43fd8af 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -2,6 +2,7 @@ FROM node:16-alpine AS BUILD_IMAGE RUN apk add curl +WORKDIR / COPY package*.json ./ COPY tsconfig.json ./ COPY /app ./app @@ -11,6 +12,7 @@ RUN npm prune --production FROM node:16-alpine +WORKDIR / COPY --from=BUILD_IMAGE /node_modules ./node_modules COPY --from=BUILD_IMAGE /dist ./dist COPY package*.json ./ diff --git a/tests/ffmpeg.ts b/tests/ffmpeg.ts index 8bc1c49..bd6c7d7 100644 --- a/tests/ffmpeg.ts +++ b/tests/ffmpeg.ts @@ -1,9 +1,13 @@ -import ffmpeg from 'fluent-ffmpeg'; -import ffmpegInstaller from '@ffmpeg-installer/ffmpeg'; -import ffprobeInstaller from '@ffprobe-installer/ffprobe'; -import which from 'which'; +import ffmpeg from "fluent-ffmpeg"; +import ffmpegInstaller from "@ffmpeg-installer/ffmpeg"; +import ffprobeInstaller from "@ffprobe-installer/ffprobe"; +import which from "which"; -const getExecutablePath = (envVar: string, executable: string, installer: { path: string }) => { +const getExecutablePath = ( + envVar: string, + executable: string, + installer: { path: string }, +) => { if (process.env[envVar]) { return process.env[envVar]; } @@ -15,8 +19,16 @@ const getExecutablePath = (envVar: string, executable: string, installer: { path } }; -const ffmpegPath = getExecutablePath('EB_FFMPEG_PATH', 'ffmpeg', ffmpegInstaller); -const ffprobePath = getExecutablePath('EB_FFPROBE_PATH', 'ffprobe', ffprobeInstaller); +const ffmpegPath = getExecutablePath( + "EB_FFMPEG_PATH", + "ffmpeg", + ffmpegInstaller, +); +const ffprobePath = getExecutablePath( + "EB_FFPROBE_PATH", + "ffprobe", + ffprobeInstaller, +); console.log(`Using ffmpeg from path: ${ffmpegPath}`); console.log(`Using ffprobe from path: ${ffprobePath}`); @@ -25,65 +37,76 @@ ffmpeg.setFfmpegPath(ffmpegPath!); ffmpeg.setFfprobePath(ffprobePath!); export enum EncodingType { - CPU = 'libx264', - NVIDIA = 'h264_nvenc', - AMD = 'h264_amf', - INTEL = 'h264_qsv', - APPLE = 'h264_videotoolbox', + CPU = "libx264", + NVIDIA = "h264_nvenc", + AMD = "h264_amf", + INTEL = "h264_qsv", + APPLE = "h264_videotoolbox", } -export const generateTestVideo = async (encodingType: EncodingType): Promise => { +export const generateTestVideo = async ( + encodingType: EncodingType, +): Promise => { console.log(`Generating test video using ${encodingType}...`); const startTime = Date.now(); let totalFrames = 0; const outputOptions = [ - '-vf', 'scale=-2:720', - '-vcodec', encodingType, - '-c:a', 'copy', - '-b:v', '5000k', - '-pix_fmt', 'yuv420p', + "-vf", + "scale=-2:720", + "-vcodec", + encodingType, + "-c:a", + "copy", + "-b:v", + "5000k", + "-pix_fmt", + "yuv420p", ]; // Adjust output options based on encoder for maximum quality - switch(encodingType) { + switch (encodingType) { case EncodingType.CPU: //outputOptions.push('-crf', '0'); break; case EncodingType.NVIDIA: - outputOptions.push('-rc', 'cqp', '-qp', '0'); + outputOptions.push("-rc", "cqp", "-qp", "0"); break; case EncodingType.AMD: - outputOptions.push('-qp_i', '0', '-qp_p', '0', '-qp_b', '0'); + outputOptions.push("-qp_i", "0", "-qp_p", "0", "-qp_b", "0"); break; case EncodingType.INTEL: - outputOptions.push('-global_quality', '1'); // Intel QSV specific setting for high quality + outputOptions.push("-global_quality", "1"); // Intel QSV specific setting for high quality break; case EncodingType.APPLE: - outputOptions.push('-global_quality', '1'); + outputOptions.push("-global_quality", "1"); break; } return new Promise((resolve, reject) => { ffmpeg() - .input('unknown_replay_2023.10.29-22.57-00.00.38.103-00.01.00.016.mp4') - .inputFormat('mp4') + .input("unknown_replay_2023.10.29-22.57-00.00.38.103-00.01.00.016.mp4") + .inputFormat("mp4") .outputOptions(outputOptions) .output(`720p-test-${encodingType}.mp4`) - .on('progress', (progress) => { + .on("progress", (progress) => { totalFrames = progress.frames; }) - .on('end', () => { - const elapsedTime = (Date.now() - startTime) / 1000; // Convert to seconds + .on("end", () => { + const elapsedTime = (Date.now() - startTime) / 1000; // Convert to seconds const avgFps = totalFrames / elapsedTime; - - console.log(`720p copy complete using ${encodingType}, took ${Date.now() - startTime}ms to complete`); + + console.log( + `720p copy complete using ${encodingType}, took ${ + Date.now() - startTime + }ms to complete`, + ); console.log(`Average FPS for the entire process: ${avgFps.toFixed(2)}`); - + resolve(); }) - .on('error', (e) => reject(new Error(e))) + .on("error", (e) => reject(new Error(e))) .run(); }); }; diff --git a/tests/test.ts b/tests/test.ts index dc99f1c..0e635d5 100644 --- a/tests/test.ts +++ b/tests/test.ts @@ -1,13 +1,13 @@ -import readline from 'readline'; +import readline from "readline"; import { generateTestVideo, EncodingType } from "./ffmpeg"; const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout + input: process.stdin, + output: process.stdout, }); const questionAsync = (query: string) => { - return new Promise(resolve => { + return new Promise((resolve) => { rl.question(query, resolve); }); }; @@ -16,27 +16,31 @@ const main = async () => { console.log("Testing software encoder: "); await generateTestVideo(EncodingType.CPU).catch(console.error); - const answer = await questionAsync('Would you like to test other hardware encoders? (yes/no): '); + const answer = await questionAsync( + "Would you like to test other hardware encoders? (yes/no): ", + ); - if (answer.toLowerCase() === 'yes') { - const encoder = await questionAsync('Which hardware encoder would you like to test? (INTEL/NVIDIA/AMD/APPLE): '); + if (answer.toLowerCase() === "yes") { + const encoder = await questionAsync( + "Which hardware encoder would you like to test? (INTEL/NVIDIA/AMD/APPLE): ", + ); let selectedEncoder: EncodingType; switch (encoder.toUpperCase()) { - case 'INTEL': + case "INTEL": selectedEncoder = EncodingType.INTEL; break; - case 'NVIDIA': + case "NVIDIA": selectedEncoder = EncodingType.NVIDIA; break; - case 'AMD': + case "AMD": selectedEncoder = EncodingType.AMD; break; - case 'APPLE': + case "APPLE": selectedEncoder = EncodingType.APPLE; break; default: - console.log('Invalid choice. Exiting.'); + console.log("Invalid choice. Exiting."); rl.close(); return; } @@ -46,11 +50,11 @@ const main = async () => { } else { console.log("Exiting."); } - + rl.close(); }; -main().catch(err => { +main().catch((err) => { console.error("An error occurred:", err); rl.close(); });