htmx is so cool
This commit is contained in:
parent
e04ef78a42
commit
3ed7b0b5c7
11 changed files with 557 additions and 400 deletions
120
app/lib/db.ts
120
app/lib/db.ts
|
@ -8,62 +8,74 @@ mkdirp.sync("./var/db");
|
||||||
export const db = new sqlite3.Database("./var/db/media.db");
|
export const db = new sqlite3.Database("./var/db/media.db");
|
||||||
|
|
||||||
/**Create the database schema for the embedders app*/
|
/**Create the database schema for the embedders app*/
|
||||||
export function createDatabase(version: number){
|
export function createDatabase(version: number) {
|
||||||
console.log("Creating database");
|
console.log("Creating database");
|
||||||
|
|
||||||
db.run("CREATE TABLE IF NOT EXISTS users ( \
|
db.run(
|
||||||
|
"CREATE TABLE IF NOT EXISTS users ( \
|
||||||
id INTEGER PRIMARY KEY, \
|
id INTEGER PRIMARY KEY, \
|
||||||
username TEXT UNIQUE, \
|
username TEXT UNIQUE, \
|
||||||
hashed_password BLOB, \
|
hashed_password BLOB, \
|
||||||
expire INTEGER, \
|
expire INTEGER, \
|
||||||
salt BLOB \
|
salt BLOB \
|
||||||
)", () => createUser("admin", process.env.EBPASS || "changeme"));
|
)",
|
||||||
|
() => createUser("admin", process.env.EBPASS || "changeme")
|
||||||
|
);
|
||||||
|
|
||||||
db.run("CREATE TABLE IF NOT EXISTS media ( \
|
db.run(
|
||||||
|
"CREATE TABLE IF NOT EXISTS media ( \
|
||||||
id INTEGER PRIMARY KEY, \
|
id INTEGER PRIMARY KEY, \
|
||||||
path TEXT NOT NULL, \
|
path TEXT NOT NULL, \
|
||||||
expire INTEGER, \
|
expire INTEGER, \
|
||||||
username TEXT \
|
username TEXT \
|
||||||
)");
|
)"
|
||||||
|
);
|
||||||
|
|
||||||
db.run(`PRAGMA user_version = ${version}`);
|
db.run(`PRAGMA user_version = ${version}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**Updates old Database schema to new */
|
/**Updates old Database schema to new */
|
||||||
export function updateDatabase(oldVersion: number, newVersion: number){
|
export function updateDatabase(oldVersion: number, newVersion: number) {
|
||||||
if (oldVersion == 1) {
|
if (oldVersion == 1) {
|
||||||
console.log(`Updating database from ${oldVersion} to ${newVersion}`);
|
console.log(`Updating database from ${oldVersion} to ${newVersion}`);
|
||||||
db.run("PRAGMA user_version = 2", (err) => {
|
db.run("PRAGMA user_version = 2", (err) => {
|
||||||
if(err) return;
|
if (err) return;
|
||||||
});
|
});
|
||||||
db.run("ALTER TABLE media ADD COLUMN username TEXT", (err) => {
|
db.run("ALTER TABLE media ADD COLUMN username TEXT", (err) => {
|
||||||
if(err) return;
|
if (err) return;
|
||||||
});
|
});
|
||||||
|
|
||||||
db.run("ALTER TABLE users ADD COLUMN expire TEXT", (err) => {
|
db.run("ALTER TABLE users ADD COLUMN expire TEXT", (err) => {
|
||||||
if(err) return;
|
if (err) return;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**Inserts into the media table */
|
/**Inserts into the media table */
|
||||||
export function insertToDB (filename: string, expireDate: Date, username: string) {
|
export function insertToDB(
|
||||||
const params: MediaParams = [
|
filename: string,
|
||||||
filename,
|
expireDate: Date,
|
||||||
expireDate,
|
username: string
|
||||||
username
|
): Promise<void> {
|
||||||
];
|
return new Promise((resolve, reject) => {
|
||||||
|
const params: MediaParams = [filename, expireDate, username];
|
||||||
db.run("INSERT INTO media (path, expire, username) VALUES (?, ?, ?)", params, function (err) {
|
|
||||||
if (err) {
|
db.run(
|
||||||
console.log(err);
|
"INSERT INTO media (path, expire, username) VALUES (?, ?, ?)",
|
||||||
return err;
|
params,
|
||||||
}
|
function (err) {
|
||||||
console.log(`Uploaded ${filename} to database`);
|
if (err) {
|
||||||
if (expireDate == null)
|
console.log(err);
|
||||||
console.log("It will not expire");
|
reject(err);
|
||||||
else if (expireDate != null || expireDate != undefined)
|
} else {
|
||||||
console.log(`It will expire on ${expireDate}`);
|
console.log(`Uploaded ${filename} to database`);
|
||||||
|
if (expireDate == null) console.log("It will not expire");
|
||||||
|
else if (expireDate != null || expireDate != undefined)
|
||||||
|
console.log(`It will expire on ${expireDate}`);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +86,7 @@ export function searchImages(imagename: string, partial: boolean) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateImageName(oldimagename: string, newname:string) {
|
export function updateImageName(oldimagename: string, newname: string) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
console.log(`updating ${oldimagename} to ${newname}`);
|
console.log(`updating ${oldimagename} to ${newname}`);
|
||||||
});
|
});
|
||||||
|
@ -85,12 +97,11 @@ export function createUser(username: string, password: string) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
console.log(`Creating user ${username}`);
|
console.log(`Creating user ${username}`);
|
||||||
const salt = crypto.randomBytes(16);
|
const salt = crypto.randomBytes(16);
|
||||||
|
|
||||||
db.run("INSERT OR IGNORE INTO users (username, hashed_password, salt) VALUES (?, ?, ?)", [
|
db.run(
|
||||||
username,
|
"INSERT OR IGNORE INTO users (username, hashed_password, salt) VALUES (?, ?, ?)",
|
||||||
crypto.pbkdf2Sync(password, salt, 310000, 32, "sha256"),
|
[username, crypto.pbkdf2Sync(password, salt, 310000, 32, "sha256"), salt]
|
||||||
salt
|
);
|
||||||
]);
|
|
||||||
|
|
||||||
resolve(null);
|
resolve(null);
|
||||||
});
|
});
|
||||||
|
@ -102,7 +113,9 @@ export function getPath(id: number | string) {
|
||||||
const query = "SELECT path FROM media WHERE id = ?";
|
const query = "SELECT path FROM media WHERE id = ?";
|
||||||
|
|
||||||
db.get(query, [id], (err: Error, path: object) => {
|
db.get(query, [id], (err: Error, path: object) => {
|
||||||
if (err) {reject(err);}
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
resolve(path);
|
resolve(path);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -114,14 +127,17 @@ export function deleteId(database: string, id: number | string) {
|
||||||
const query = `DELETE FROM ${database} WHERE id = ?`;
|
const query = `DELETE FROM ${database} WHERE id = ?`;
|
||||||
|
|
||||||
db.run(query, [id], (err: Error) => {
|
db.run(query, [id], (err: Error) => {
|
||||||
if (err) {reject(err); return;}
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
resolve(null);
|
resolve(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**Expires a database row given a Date in unix time */
|
/**Expires a database row given a Date in unix time */
|
||||||
export function expire(database: string, column: string, expiration:number) {
|
export function expire(database: string, column: string, expiration: number) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const query = `SELECT * FROM ${database} WHERE ${column} < ?`;
|
const query = `SELECT * FROM ${database} WHERE ${column} < ?`;
|
||||||
|
|
||||||
|
@ -136,30 +152,26 @@ export function expire(database: string, column: string, expiration:number) {
|
||||||
|
|
||||||
/**A generic database row */
|
/**A generic database row */
|
||||||
export interface GenericRow {
|
export interface GenericRow {
|
||||||
id? : number | string,
|
id?: number | string;
|
||||||
username?: string
|
username?: string;
|
||||||
expire? :Date
|
expire?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**A row for the media database */
|
/**A row for the media database */
|
||||||
export interface MediaRow {
|
export interface MediaRow {
|
||||||
id? : number | string,
|
id?: number | string;
|
||||||
path: string,
|
path: string;
|
||||||
expire: Date,
|
expire: Date;
|
||||||
username?: string
|
username?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**Params type for doing work with media database */
|
/**Params type for doing work with media database */
|
||||||
export type MediaParams = [
|
export type MediaParams = [path: string, expire: Date, username?: string];
|
||||||
path: string,
|
|
||||||
expire: Date,
|
|
||||||
username?: string
|
|
||||||
]
|
|
||||||
|
|
||||||
/**A row for the user database */
|
/**A row for the user database */
|
||||||
export interface UserRow {
|
export interface UserRow {
|
||||||
id? : number | string,
|
id?: number | string;
|
||||||
username: string,
|
username: string;
|
||||||
hashed_password: any,
|
hashed_password: any;
|
||||||
salt: any
|
salt: any;
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,8 +54,8 @@ export const setEncodingType = (type: EncodingType) => {
|
||||||
const getExecutablePath = (
|
const getExecutablePath = (
|
||||||
envVar: string,
|
envVar: string,
|
||||||
executable: string,
|
executable: string,
|
||||||
installer: { path: string }
|
installer: { path: string },
|
||||||
) => {
|
): string => {
|
||||||
if (process.env[envVar]) {
|
if (process.env[envVar]) {
|
||||||
return process.env[envVar];
|
return process.env[envVar];
|
||||||
}
|
}
|
||||||
|
@ -70,12 +70,13 @@ const getExecutablePath = (
|
||||||
const ffmpegPath = getExecutablePath(
|
const ffmpegPath = getExecutablePath(
|
||||||
"EB_FFMPEG_PATH",
|
"EB_FFMPEG_PATH",
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
ffmpegInstaller
|
ffmpegInstaller,
|
||||||
);
|
);
|
||||||
|
|
||||||
const ffprobePath = getExecutablePath(
|
const ffprobePath = getExecutablePath(
|
||||||
"EB_FFPROBE_PATH",
|
"EB_FFPROBE_PATH",
|
||||||
"ffprobe",
|
"ffprobe",
|
||||||
ffprobeInstaller
|
ffprobeInstaller,
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`Using ffmpeg from path: ${ffmpegPath}`);
|
console.log(`Using ffmpeg from path: ${ffmpegPath}`);
|
||||||
|
@ -89,14 +90,14 @@ const checkEnvForEncoder = () => {
|
||||||
|
|
||||||
if (envEncoder && Object.keys(EncodingType).includes(envEncoder)) {
|
if (envEncoder && Object.keys(EncodingType).includes(envEncoder)) {
|
||||||
setEncodingType(
|
setEncodingType(
|
||||||
EncodingType[envEncoder as keyof typeof EncodingType] as EncodingType
|
EncodingType[envEncoder as keyof typeof EncodingType] as EncodingType,
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
`Setting encoding type to ${envEncoder} based on environment variable.`
|
`Setting encoding type to ${envEncoder} based on environment variable.`,
|
||||||
);
|
);
|
||||||
} else if (envEncoder) {
|
} else if (envEncoder) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Invalid encoder value "${envEncoder}" in environment variable, defaulting to CPU.`
|
`Invalid encoder value "${envEncoder}" in environment variable, defaulting to CPU.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -121,7 +122,7 @@ checkEnvForEncoder();
|
||||||
export const ffmpegDownscale = (
|
export const ffmpegDownscale = (
|
||||||
path: string,
|
path: string,
|
||||||
filename: string,
|
filename: string,
|
||||||
extension: string
|
extension: string,
|
||||||
) => {
|
) => {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const outputOptions = [
|
const outputOptions = [
|
||||||
|
@ -136,32 +137,31 @@ export const ffmpegDownscale = (
|
||||||
];
|
];
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const progressFile = `uploads/${filename}${extension}-progress.json`;
|
||||||
|
|
||||||
ffmpeg()
|
ffmpeg()
|
||||||
.input(path)
|
.input(path)
|
||||||
.outputOptions(outputOptions)
|
.outputOptions(outputOptions)
|
||||||
.output(`uploads/720p-${filename}${extension}`)
|
.output(`uploads/720p-${filename}${extension}`)
|
||||||
.on("start", () => {
|
.on("progress", function(progress) {
|
||||||
// Create the .processing file
|
fs.writeFileSync(progressFile, JSON.stringify({ progress: progress.percent / 100 }));
|
||||||
fs.closeSync(
|
|
||||||
fs.openSync(`uploads/720p-${filename}${extension}.processing`, "w")
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.on("end", () => {
|
.on("end", () => {
|
||||||
console.log(
|
console.log(
|
||||||
`720p copy complete using ${currentEncoding}, took ${
|
`720p copy complete using ${currentEncoding}, took ${
|
||||||
Date.now() - startTime
|
Date.now() - startTime
|
||||||
}ms to complete`
|
}ms to complete`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete the .processing file
|
// Delete the .processing file
|
||||||
fs.unlinkSync(`uploads/720p-${filename}${extension}.processing`);
|
fs.unlinkSync(progressFile);
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.on("error", (e) => {
|
.on("error", (e) => {
|
||||||
// Ensure to delete the .processing file even on error
|
// Ensure to delete the .processing file even on error
|
||||||
if (fs.existsSync(`uploads/720p-${filename}${extension}.processing`)) {
|
if (fs.existsSync(progressFile)) {
|
||||||
fs.unlinkSync(`uploads/720p-${filename}${extension}.processing`);
|
fs.unlinkSync(progressFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
reject(new Error(e));
|
reject(new Error(e));
|
||||||
|
@ -188,7 +188,7 @@ export const ffmpegDownscale = (
|
||||||
export const ffmpegConvert = (
|
export const ffmpegConvert = (
|
||||||
path: string,
|
path: string,
|
||||||
filename: string,
|
filename: string,
|
||||||
extension: string
|
extension: string,
|
||||||
) => {
|
) => {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const outputOptions = [
|
const outputOptions = [
|
||||||
|
@ -217,15 +217,20 @@ export const ffmpegConvert = (
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const progressFile = `uploads/${filename}${extension}-progress.json`;
|
||||||
|
|
||||||
ffmpeg()
|
ffmpeg()
|
||||||
.input(path)
|
.input(path)
|
||||||
.outputOptions(outputOptions)
|
.outputOptions(outputOptions)
|
||||||
.output("uploads/")
|
.output("uploads/")
|
||||||
.outputFormat(outputFormat)
|
.outputFormat(outputFormat)
|
||||||
.output(`uploads/${filename}${outputFormat}`)
|
.output(`uploads/${filename}${outputFormat}`)
|
||||||
|
.on("progress", function(progress) {
|
||||||
|
fs.writeFileSync(progressFile, JSON.stringify({ progress: progress.percent / 100 }));
|
||||||
|
})
|
||||||
.on("end", function () {
|
.on("end", function () {
|
||||||
console.log(
|
console.log(
|
||||||
`Conversion complete, took ${Date.now() - startTime} to complete`
|
`Conversion complete, took ${Date.now() - startTime} to complete`,
|
||||||
);
|
);
|
||||||
console.log(`uploads/${filename}${outputFormat}`);
|
console.log(`uploads/${filename}${outputFormat}`);
|
||||||
resolve();
|
resolve();
|
||||||
|
@ -238,9 +243,14 @@ export const ffmpegConvert = (
|
||||||
export const ffProbe = async (
|
export const ffProbe = async (
|
||||||
path: string,
|
path: string,
|
||||||
filename: string,
|
filename: string,
|
||||||
extension: string
|
extension: string,
|
||||||
) => {
|
) => {
|
||||||
return new Promise<FfprobeData>((resolve, reject) => {
|
return new Promise<FfprobeData>((resolve, reject) => {
|
||||||
|
if (!videoExtensions.includes(extension) && !imageExtensions.includes(extension)) {
|
||||||
|
console.log(`Extension is ${extension}`);
|
||||||
|
reject(`Submitted file is neither a video nor an image: ${path}`);
|
||||||
|
}
|
||||||
|
|
||||||
ffprobe(path, (err, data) => {
|
ffprobe(path, (err, data) => {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
resolve(data);
|
resolve(data);
|
||||||
|
|
|
@ -25,6 +25,17 @@ export interface User {
|
||||||
salt?: any;
|
salt?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface oembedObj {
|
||||||
|
type: string;
|
||||||
|
version: string;
|
||||||
|
provider_name: string;
|
||||||
|
provider_url: string;
|
||||||
|
cache_age: number;
|
||||||
|
html: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const videoExtensions = [
|
export const videoExtensions = [
|
||||||
".mp4",
|
".mp4",
|
||||||
".mov",
|
".mov",
|
||||||
|
|
|
@ -3,7 +3,7 @@ import type { RequestHandler as Middleware, NextFunction } from "express";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import process from "process";
|
import process from "process";
|
||||||
|
|
||||||
import { extension, videoExtensions, imageExtensions } from "./lib";
|
import { extension, videoExtensions, imageExtensions, oembedObj } from "./lib";
|
||||||
import { insertToDB } from "./db";
|
import { insertToDB } from "./db";
|
||||||
import { ffmpegDownscale, ffProbe } from "./ffmpeg";
|
import { ffmpegDownscale, ffProbe } from "./ffmpeg";
|
||||||
import { ffprobe } from "fluent-ffmpeg";
|
import { ffprobe } from "fluent-ffmpeg";
|
||||||
|
@ -50,15 +50,9 @@ export const createEmbedData: Middleware = async (req, res, next) => {
|
||||||
const files = req.files as Express.Multer.File[];
|
const files = req.files as Express.Multer.File[];
|
||||||
for (const file in files) {
|
for (const file in files) {
|
||||||
const nameAndExtension = extension(files[file].originalname);
|
const nameAndExtension = extension(files[file].originalname);
|
||||||
const ffProbeData = await ffProbe(
|
const isMedia = videoExtensions.includes(nameAndExtension[1]) || imageExtensions.includes(nameAndExtension[1]);
|
||||||
`uploads/${files[file].originalname}`,
|
|
||||||
nameAndExtension[0],
|
|
||||||
nameAndExtension[1],
|
|
||||||
);
|
|
||||||
const width = ffProbeData.streams[0].width;
|
|
||||||
const height = ffProbeData.streams[0].height;
|
|
||||||
|
|
||||||
const oembed = {
|
const oembed: oembedObj = {
|
||||||
type: "video",
|
type: "video",
|
||||||
version: "1.0",
|
version: "1.0",
|
||||||
provider_name: "embedder",
|
provider_name: "embedder",
|
||||||
|
@ -66,11 +60,24 @@ export const createEmbedData: Middleware = async (req, res, next) => {
|
||||||
cache_age: 86400,
|
cache_age: 86400,
|
||||||
html: `<iframe src='${req.protocol}://${req.get("host")}/gifv/${
|
html: `<iframe src='${req.protocol}://${req.get("host")}/gifv/${
|
||||||
nameAndExtension[0]
|
nameAndExtension[0]
|
||||||
}${nameAndExtension[1]}'></iframe>`,
|
}${nameAndExtension[1]}'></iframe>`
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isMedia) {
|
||||||
|
let ffProbeData;
|
||||||
|
try { ffProbeData = await ffProbe(
|
||||||
|
`uploads/${files[file].originalname}`,
|
||||||
|
nameAndExtension[0],
|
||||||
|
nameAndExtension[1],
|
||||||
|
); } catch (error) {
|
||||||
|
console.log(`Error: ${error}`);
|
||||||
|
console.log(nameAndExtension[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
oembed.width = ffProbeData.streams[0].width;
|
||||||
|
oembed.height = ffProbeData.streams[0].height;
|
||||||
|
}
|
||||||
|
|
||||||
fs.writeFile(
|
fs.writeFile(
|
||||||
`uploads/oembed-${nameAndExtension[0]}${nameAndExtension[1]}.json`,
|
`uploads/oembed-${nameAndExtension[0]}${nameAndExtension[1]}.json`,
|
||||||
JSON.stringify(oembed),
|
JSON.stringify(oembed),
|
||||||
|
@ -121,23 +128,27 @@ export const convertTo720p: Middleware = (req, res, next) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**Middleware for handling uploaded files. Inserts it into the database */
|
/**Middleware for handling uploaded files. Inserts it into the database */
|
||||||
export const handleUpload: Middleware = (req, res, next) => {
|
export const handleUpload: Middleware = async (req, res, next) => {
|
||||||
if (!req.file && !req.files) {
|
if (!req.file && !req.files) {
|
||||||
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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = req.files ? (req.files as Express.Multer.File[]) : req.file; //Check if a single file was uploaded or multiple
|
const files = req.files ? (req.files as Express.Multer.File[]) : req.file;
|
||||||
const username = req.user ? req.user.username : "sharex"; //if no username was provided, we can presume that it is sharex
|
const username = req.user ? req.user.username : "sharex";
|
||||||
const expireDate: Date = req.body.expire
|
const expireDate: Date = req.body.expire
|
||||||
? new Date(Date.now() + req.body.expire * 24 * 60 * 60 * 1000)
|
? new Date(Date.now() + req.body.expire * 24 * 60 * 60 * 1000)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (files instanceof Array) {
|
try {
|
||||||
for (const file in files) {
|
if (files instanceof Array) {
|
||||||
insertToDB(files[file].filename, expireDate, username);
|
await Promise.all(files.map(file => insertToDB(file.filename, expireDate, username)));
|
||||||
|
} else {
|
||||||
|
await insertToDB(files.filename, expireDate, username);
|
||||||
}
|
}
|
||||||
} else insertToDB(files.filename, expireDate, username);
|
next();
|
||||||
|
} catch (error) {
|
||||||
next();
|
console.error("Error in handleUpload:", error);
|
||||||
|
res.status(500).send("Error processing files.");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,6 +21,60 @@
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
/* Positioning and Sizing */
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
position: relative;
|
||||||
|
margin: 50px auto;
|
||||||
|
/* Centering the spinner */
|
||||||
|
|
||||||
|
/* Text Styling */
|
||||||
|
color: #555;
|
||||||
|
text-align: center;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
padding-top: 80px;
|
||||||
|
/* Adjust as needed for text position */
|
||||||
|
|
||||||
|
/* Adding a background to the spinner for better visibility */
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keyframes for the spinner animation */
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spinner Animation */
|
||||||
|
.spinner::before {
|
||||||
|
content: '';
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 40px;
|
||||||
|
/* Spinner Size */
|
||||||
|
height: 40px;
|
||||||
|
margin-top: -20px;
|
||||||
|
/* Half of height */
|
||||||
|
margin-left: -20px;
|
||||||
|
/* Half of width */
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-top-color: #007bff;
|
||||||
|
/* Spinner Color */
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
#search {
|
#search {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
background: rgb(31, 32, 35);
|
background: rgb(31, 32, 35);
|
||||||
|
|
|
@ -1,59 +1,74 @@
|
||||||
|
/* eslint-disable no-undef */
|
||||||
/* eslint-env browser: true */
|
/* eslint-env browser: true */
|
||||||
|
|
||||||
|
let files;
|
||||||
|
|
||||||
function copyURI(evt) {
|
function copyURI(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
navigator.clipboard.writeText(absolutePath(evt.target.getAttribute("src"))).then(() => {
|
navigator.clipboard
|
||||||
/* clipboard successfully set */
|
.writeText(absolutePath(evt.target.getAttribute("src")))
|
||||||
console.log("copied");
|
.then(
|
||||||
}, () => {
|
() => {
|
||||||
/* clipboard write failed */
|
/* clipboard successfully set */
|
||||||
console.log("failed");
|
console.log("copied");
|
||||||
});
|
},
|
||||||
|
() => {
|
||||||
|
/* clipboard write failed */
|
||||||
|
console.log("failed");
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyA(evt) {
|
function copyA(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
navigator.clipboard.writeText(absolutePath(evt.target.getAttribute("href"))).then(() => {
|
navigator.clipboard
|
||||||
console.log("copied");
|
.writeText(absolutePath(evt.target.getAttribute("href")))
|
||||||
}, () => {
|
.then(
|
||||||
console.log("failed");
|
() => {
|
||||||
});
|
console.log("copied");
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
console.log("failed");
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyPath(evt) {
|
function copyPath(evt) {
|
||||||
navigator.clipboard.writeText(absolutePath(evt)).then(() => {
|
navigator.clipboard.writeText(absolutePath(evt)).then(
|
||||||
console.log("copied");
|
() => {
|
||||||
}, () => {
|
console.log("copied");
|
||||||
console.log("failed");
|
},
|
||||||
});
|
() => {
|
||||||
|
console.log("failed");
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function absolutePath (href) {
|
function absolutePath(href) {
|
||||||
let link = document.createElement("a");
|
let link = document.createElement("a");
|
||||||
link.href = href;
|
link.href = href;
|
||||||
return link.href;
|
return link.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extension(string) {
|
function extension(string) {
|
||||||
return string.slice((string.lastIndexOf(".") - 2 >>> 0) + 2);
|
return string.slice(((string.lastIndexOf(".") - 2) >>> 0) + 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
let dropArea = document.getElementById("dropArea");
|
let dropArea = document.getElementById("dropArea");
|
||||||
|
|
||||||
["dragenter", "dragover", "dragleave", "drop"].forEach(eventName => {
|
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
|
||||||
dropArea.addEventListener(eventName, preventDefaults, false);
|
dropArea.addEventListener(eventName, preventDefaults, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
function preventDefaults (e) {
|
function preventDefaults(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
["dragenter", "dragover"].forEach(eventName => {
|
["dragenter", "dragover"].forEach((eventName) => {
|
||||||
dropArea.addEventListener(eventName, highlight, false);
|
dropArea.addEventListener(eventName, highlight, false);
|
||||||
})
|
});
|
||||||
|
["dragleave", "drop"].forEach((eventName) => {
|
||||||
;["dragleave", "drop"].forEach(eventName => {
|
|
||||||
dropArea.addEventListener(eventName, unhighlight, false);
|
dropArea.addEventListener(eventName, unhighlight, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -100,16 +115,14 @@ function handleFiles(files) {
|
||||||
files.forEach(previewFile);
|
files.forEach(previewFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function previewFile(file) {
|
function previewFile(file) {
|
||||||
let reader = new FileReader();
|
let reader = new FileReader();
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
reader.onloadend = function() {
|
reader.onloadend = function () {
|
||||||
let img = document.createElement("img");
|
let img = document.createElement("img");
|
||||||
img.src = reader.result;
|
img.src = reader.result;
|
||||||
img.className = "image";
|
img.className = "image";
|
||||||
document.getElementById("gallery").appendChild(img);
|
document.getElementById("gallery").appendChild(img);
|
||||||
console.log(document.getElementById("fileupload"));
|
|
||||||
document.getElementById("fileupload").src = img.src;
|
document.getElementById("fileupload").src = img.src;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -118,15 +131,20 @@ function uploadFile(file) {
|
||||||
let xhr = new XMLHttpRequest();
|
let xhr = new XMLHttpRequest();
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
let reader = new FileReader();
|
let reader = new FileReader();
|
||||||
|
|
||||||
xhr.open("POST", "/", true);
|
xhr.open("POST", "/", true);
|
||||||
|
|
||||||
xhr.addEventListener("readystatechange", function(e) {
|
xhr.addEventListener("readystatechange", function (e) {
|
||||||
if (xhr.readyState == 4 && xhr.status == 200) {
|
if (xhr.readyState == 4) {
|
||||||
location.reload();
|
if (xhr.status == 200) {
|
||||||
}
|
let response = xhr.responseText;
|
||||||
else if (xhr.readyState == 4 && xhr.status != 200) {
|
//document.getElementById("embedder-list").innerHTML = response;
|
||||||
alert(`Upload failed, error code: ${xhr.status}`)
|
htmx.ajax("GET", "/media-list", {target: "#embedder-list", swap: "innerHTML"});
|
||||||
|
document.getElementById("gallery").innerHTML = "";
|
||||||
|
htmx.process(document.body);
|
||||||
|
} else {
|
||||||
|
alert(`Upload failed, error code: ${xhr.status}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -136,7 +154,6 @@ function uploadFile(file) {
|
||||||
|
|
||||||
formData.append("fileupload", file);
|
formData.append("fileupload", file);
|
||||||
formData.append("expire", document.getElementById("expire").value);
|
formData.append("expire", document.getElementById("expire").value);
|
||||||
console.log(formData);
|
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,18 +166,27 @@ function openFullSize(imageUrl) {
|
||||||
video.src = imageUrl;
|
video.src = imageUrl;
|
||||||
video.controls = true;
|
video.controls = true;
|
||||||
|
|
||||||
if (extension(imageUrl) == ".jpg" || extension(imageUrl) == ".png" || extension(imageUrl) == ".gif" || extension(imageUrl) == ".jpeg" || extension(imageUrl) == ".webp") {
|
if (
|
||||||
|
extension(imageUrl) == ".jpg" ||
|
||||||
|
extension(imageUrl) == ".png" ||
|
||||||
|
extension(imageUrl) == ".gif" ||
|
||||||
|
extension(imageUrl) == ".jpeg" ||
|
||||||
|
extension(imageUrl) == ".webp"
|
||||||
|
) {
|
||||||
modal.appendChild(img);
|
modal.appendChild(img);
|
||||||
}
|
} else if (
|
||||||
else if (extension(imageUrl) == ".mp4" || extension(imageUrl) == ".webm" || extension(imageUrl) == ".mov") {
|
extension(imageUrl) == ".mp4" ||
|
||||||
|
extension(imageUrl) == ".webm" ||
|
||||||
|
extension(imageUrl) == ".mov"
|
||||||
|
) {
|
||||||
modal.appendChild(video);
|
modal.appendChild(video);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the modal to the page
|
// Add the modal to the page
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
// Add an event listener to close the modal when the user clicks on it
|
// Add an event listener to close the modal when the user clicks on it
|
||||||
modal.addEventListener("click", function() {
|
modal.addEventListener("click", function () {
|
||||||
modal.remove();
|
modal.remove();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -172,20 +198,112 @@ searchInput.addEventListener("input", () => {
|
||||||
let mediaList = document.querySelectorAll("ul.embedder-list li");
|
let mediaList = document.querySelectorAll("ul.embedder-list li");
|
||||||
|
|
||||||
mediaList.forEach((li) => {
|
mediaList.forEach((li) => {
|
||||||
if (!li.id.toLowerCase().includes(searchValue)) { //make lowercase to allow case insensitivity
|
if (!li.id.toLowerCase().includes(searchValue)) {
|
||||||
|
//make lowercase to allow case insensitivity
|
||||||
li.classList.add("hide");
|
li.classList.add("hide");
|
||||||
li.classList.remove("show");
|
li.classList.remove("show");
|
||||||
li.addEventListener("animationend", function() {
|
li.addEventListener(
|
||||||
if (searchInput.value !== "") {
|
"animationend",
|
||||||
this.style.display = "none";
|
function () {
|
||||||
}
|
if (searchInput.value !== "") {
|
||||||
}, {once: true}); // The {once: true} option automatically removes the event listener after it has been called
|
this.style.display = "none";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
); // The {once: true} option automatically removes the event listener after it has been called
|
||||||
} else {
|
} else {
|
||||||
li.style.display = "";
|
li.style.display = "";
|
||||||
li.classList.remove("hide");
|
li.classList.remove("hide");
|
||||||
if (searchValue === "" && !li.classList.contains("show")) {
|
if (searchValue === "" && !li.classList.contains("show")) {
|
||||||
li.classList.add("show");
|
li.classList.add("show");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function checkFileAvailability(filePath) {
|
||||||
|
const checkFile = () => {
|
||||||
|
console.log(`Checking if ${filePath} is processed...`);
|
||||||
|
fetch(`/uploads/${filePath}-progress.json`)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
console.log(`${filePath} still processing`);
|
||||||
|
return response.json();
|
||||||
|
} else {
|
||||||
|
console.log(`${filePath} finished processing`);
|
||||||
|
console.log(`/uploads/720p-${filePath}-progress.json finished`);
|
||||||
|
clearInterval(interval);
|
||||||
|
createVideoElement(filePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((jsonData) => {
|
||||||
|
// Handle your JSON data here
|
||||||
|
console.log(jsonData);
|
||||||
|
})
|
||||||
|
.catch((error) => console.error("Error:", error));
|
||||||
|
};
|
||||||
|
|
||||||
|
checkFile();
|
||||||
|
const interval = setInterval(checkFile, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createVideoElement(filePath) {
|
||||||
|
const videoContainer = document.getElementById(`video-${filePath}`);
|
||||||
|
videoContainer.outerHTML = `
|
||||||
|
<video id='video-${filePath}' class="image" autoplay loop muted playsinline loading="lazy">
|
||||||
|
<source src="/uploads/720p-${filePath}" loading="lazy">
|
||||||
|
</video>
|
||||||
|
`;
|
||||||
|
videoContainer.style.display = "block";
|
||||||
|
document.getElementById(`spinner-${filePath}`).style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateMediaList() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/media-list");
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.text();
|
||||||
|
|
||||||
|
document.getElementById("embedder-list").innerHTML = data;
|
||||||
|
htmx.process(document.body);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("There was a problem with the fetch operation:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshMediaList(files) {
|
||||||
|
files.forEach(file => {
|
||||||
|
console.log(`Checking ${file.path}...`);
|
||||||
|
if (videoExtensions.includes(extension(file.path))) {
|
||||||
|
const progressFileName = `uploads/${file.path}-progress.json`;
|
||||||
|
console.log(`Fetching ${progressFileName}...`);
|
||||||
|
checkFileAvailability(file.path);
|
||||||
|
} else {
|
||||||
|
console.log(`File ${file.path} is not a video, displaying...`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoExtensions = [
|
||||||
|
".mp4",
|
||||||
|
".mov",
|
||||||
|
".avi",
|
||||||
|
".flv",
|
||||||
|
".mkv",
|
||||||
|
".wmv",
|
||||||
|
".webm",
|
||||||
|
];
|
||||||
|
const imageExtensions = [
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".png",
|
||||||
|
".gif",
|
||||||
|
".bmp",
|
||||||
|
".svg",
|
||||||
|
".tiff",
|
||||||
|
".webp",
|
||||||
|
];
|
|
@ -12,6 +12,7 @@ import imageProbe from "probe-image-size";
|
||||||
import { ffProbe } from "../lib/ffmpeg";
|
import { ffProbe } from "../lib/ffmpeg";
|
||||||
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
import { extension, videoExtensions } from "../lib/lib";
|
import { extension, videoExtensions } from "../lib/lib";
|
||||||
import { db, MediaRow, getPath, deleteId } from "../lib/db";
|
import { db, MediaRow, getPath, deleteId } from "../lib/db";
|
||||||
|
@ -26,6 +27,7 @@ import {
|
||||||
|
|
||||||
const upload = multer({ storage: fileStorage /**, fileFilter: fileFilter**/ }); //maybe make this a env variable?
|
const upload = multer({ storage: fileStorage /**, fileFilter: fileFilter**/ }); //maybe make this a env variable?
|
||||||
/**Middleware to grab media from media database */
|
/**Middleware to grab media from media database */
|
||||||
|
|
||||||
const fetchMedia: Middleware = (req, res, next) => {
|
const fetchMedia: Middleware = (req, res, next) => {
|
||||||
const admin: boolean = req.user.username == "admin" ? true : false;
|
const admin: boolean = req.user.username == "admin" ? true : false;
|
||||||
/**Check if the user is an admin, if so, show all posts from all users */
|
/**Check if the user is an admin, if so, show all posts from all users */
|
||||||
|
@ -63,12 +65,12 @@ router.get(
|
||||||
(req: Request, res: Response) => {
|
(req: Request, res: Response) => {
|
||||||
res.locals.filter = null;
|
res.locals.filter = null;
|
||||||
res.render("index", { user: req.user });
|
res.render("index", { user: req.user });
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/*router.get("/media-list", fetchMedia, (req: Request, res: Response) => {
|
router.get("/media-list", fetchMedia, (req: Request, res: Response) => {
|
||||||
res.render("partials/_fileList"); // Render only the file list partial
|
res.render("partials/_fileList", { user: req.user }); // Render only the file list partial
|
||||||
});*/
|
});
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/gifv/:file",
|
"/gifv/:file",
|
||||||
|
@ -89,7 +91,7 @@ router.get(
|
||||||
const imageData = ffProbe(
|
const imageData = ffProbe(
|
||||||
`uploads/${req.params.file}`,
|
`uploads/${req.params.file}`,
|
||||||
nameAndExtension[0],
|
nameAndExtension[0],
|
||||||
nameAndExtension[1],
|
nameAndExtension[1]
|
||||||
);
|
);
|
||||||
|
|
||||||
width = (await imageData).streams[0].width;
|
width = (await imageData).streams[0].width;
|
||||||
|
@ -103,7 +105,7 @@ router.get(
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const imageData = await imageProbe(
|
const imageData = await imageProbe(
|
||||||
fs.createReadStream(`uploads/${req.params.file}`),
|
fs.createReadStream(`uploads/${req.params.file}`)
|
||||||
);
|
);
|
||||||
return res.render("gifv", {
|
return res.render("gifv", {
|
||||||
url: url,
|
url: url,
|
||||||
|
@ -112,7 +114,7 @@ router.get(
|
||||||
height: imageData.height,
|
height: imageData.height,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
|
@ -123,10 +125,11 @@ router.post(
|
||||||
convertTo720p,
|
convertTo720p,
|
||||||
createEmbedData,
|
createEmbedData,
|
||||||
handleUpload,
|
handleUpload,
|
||||||
|
fetchMedia,
|
||||||
],
|
],
|
||||||
(req: Request, res: Response) => {
|
(req: Request, res: Response) => {
|
||||||
res.redirect("/");
|
return res.render("partials/_fileList", { user: req.user }); // Render only the file list partial
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
|
@ -134,40 +137,63 @@ router.post(
|
||||||
[checkSharexAuth, upload.single("fileupload"), createEmbedData, handleUpload],
|
[checkSharexAuth, upload.single("fileupload"), createEmbedData, handleUpload],
|
||||||
(req: Request, res: Response) => {
|
(req: Request, res: Response) => {
|
||||||
return res.send(
|
return res.send(
|
||||||
`${req.protocol}://${req.get("host")}/uploads/${req.file.filename}`,
|
`${req.protocol}://${req.get("host")}/uploads/${req.file.filename}`
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
router.post(
|
router.get(
|
||||||
"/:id(\\d+)/delete",
|
"/:id(\\d+)/delete",
|
||||||
[checkAuth],
|
[checkAuth],
|
||||||
async (req: Request, res: Response) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const path: any = await getPath(req.params.id);
|
const filename: any = await getPath(req.params.id);
|
||||||
|
console.log(filename);
|
||||||
|
const filePath = path.join(__dirname , "../../uploads/" + filename.path);
|
||||||
|
const oembed = path.join(
|
||||||
|
__dirname , "../../uploads/oembed-" + filename.path + ".json"
|
||||||
|
);
|
||||||
|
|
||||||
const nameAndExtension = extension(path.path);
|
const nameAndExtension = extension(filePath);
|
||||||
|
const filesToDelete = [filePath, oembed];
|
||||||
const filesToDelete = [path.path, "oembed-" + path.path + ".json"];
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
videoExtensions.includes(nameAndExtension[1]) ||
|
videoExtensions.includes(nameAndExtension[1]) ||
|
||||||
nameAndExtension[1] == ".gif"
|
nameAndExtension[1] == ".gif"
|
||||||
) {
|
) {
|
||||||
filesToDelete.push("720p-" + path.path);
|
filesToDelete.push(
|
||||||
|
path.join(__dirname , "../../uploads/720p-" + filename.path)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
filesToDelete.forEach((path) => {
|
// Wait for all file deletions and database operations to complete
|
||||||
fs.unlink(path, async (err) => {
|
await Promise.all(
|
||||||
console.log(`Deleting ${path}`);
|
filesToDelete.map(async (path) => {
|
||||||
if (err && err.errno == -4058) {
|
return new Promise<void>((resolve, reject) => {
|
||||||
await deleteId("media", req.params.id);
|
fs.unlink(path, async (err) => {
|
||||||
}
|
console.log(`Deleting ${path}`);
|
||||||
await deleteId("media", req.params.id);
|
if (err) {
|
||||||
});
|
if ([-4058, -2].includes(err.errno)) {
|
||||||
});
|
//file not found
|
||||||
|
console.log("File not found, deleting from database");
|
||||||
|
await deleteId("media", req.params.id);
|
||||||
|
}
|
||||||
|
console.error(`Error deleting file ${path}:`, err);
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await deleteId("media", req.params.id);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return res.redirect("/");
|
next();
|
||||||
},
|
},
|
||||||
|
[fetchMedia],
|
||||||
|
(req: Request, res: Response) => {
|
||||||
|
return res.render("partials/_fileList", { user: req.user });
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -1,75 +1,87 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta charset="utf-8">
|
||||||
<title>Embedder</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="/css/base.css">
|
<title>Embedder</title>
|
||||||
<link rel="stylesheet" href="/css/index.css">
|
<link rel="stylesheet" href="/css/base.css">
|
||||||
<link rel="stylesheet" href="/css/app.css">
|
<link rel="stylesheet" href="/css/index.css">
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
<link rel="stylesheet" href="/css/app.css">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||||
<link rel="manifest" href="/site.webmanifest">
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.8"></script>
|
<link rel="manifest" href="/site.webmanifest">
|
||||||
</head>
|
<script src="https://unpkg.com/htmx.org@1.9.8"></script>
|
||||||
<body>
|
|
||||||
<section class="embedderapp">
|
</head>
|
||||||
<header class="header">
|
|
||||||
<h1>Embedder</h1>
|
<body>
|
||||||
<nav class="nav">
|
<section class="embedderapp">
|
||||||
<ul class="left-ul">
|
<header class="header">
|
||||||
<li class="user"><%= user.name || user.username %></li>
|
<h1>Embedder</h1>
|
||||||
<li>
|
<nav class="nav">
|
||||||
<form action="/logout" method="post">
|
<ul class="left-ul">
|
||||||
<button class="logout" type="submit"></button>
|
<li class="user"><%= user.name || user.username %></li>
|
||||||
</form>
|
<li>
|
||||||
</li>
|
<form action="/logout" method="post">
|
||||||
<% if (user.name == "admin" || user.username == "admin") { %>
|
<button class="logout" type="submit"></button>
|
||||||
<li>
|
</form>
|
||||||
<button class="adduser" onclick="location.href='/adduser';"></a></button>
|
</li>
|
||||||
</li>
|
<% if (user.name == "admin" || user.username == "admin") { %>
|
||||||
<% } %>
|
<li>
|
||||||
</ul>
|
<button class="adduser" onclick="location.href='/adduser';"></a></button>
|
||||||
<input type="text" id="search" name="search" placeholder="Search" value=""/>
|
</li>
|
||||||
</nav>
|
<% } %>
|
||||||
</header>
|
</ul>
|
||||||
<form action="/" method="post" encType="multipart/form-data">
|
<input type="text" id="search" name="search" placeholder="Search" value="" />
|
||||||
<div id="dropArea">
|
</nav>
|
||||||
<p class="dragregion">Upload a file, copy paste, or drag n' drop into the dashed region</p>
|
</header>
|
||||||
<div id="gallery"></div>
|
<form hx-post="/" encType="multipart/form-data" hx-target="#embedder-list" hx-swap="innerHTML">
|
||||||
<p class="dragregion"><input class="" type="file" id="fileupload" name="fileupload"><input type="button" value="Upload" id="submit" onclick="uploadFile()"></p>
|
<div id="dropArea">
|
||||||
<br>
|
<p class="dragregion">Upload a file, copy paste, or drag n' drop into the dashed region</p>
|
||||||
<br>
|
<div id="gallery"></div>
|
||||||
<p class="dragregion">
|
<p class="dragregion"><input class="" type="file" id="fileupload" name="fileupload"><input type="button" value="Upload" id="submit" onclick="uploadFile()"></p>
|
||||||
Select file expiration date:
|
<br>
|
||||||
<select name="expire" id="expire">
|
<br>
|
||||||
<option value="0.00069">1 minute</option>
|
<p class="dragregion">
|
||||||
<option value="0.00347">5 minutes</option>
|
Select file expiration date:
|
||||||
<option value="0.0417">1 hour</option>
|
<select name="expire" id="expire">
|
||||||
<option value="0.25">6 hours</option>
|
<option value="0.00069">1 minute</option>
|
||||||
<option value="1">1 day</option>
|
<option value="0.00347">5 minutes</option>
|
||||||
<option value="7">7 days</option>
|
<option value="0.0417">1 hour</option>
|
||||||
<option value="14">14 days</option>
|
<option value="0.25">6 hours</option>
|
||||||
<option value="30">30 days</option>
|
<option value="1">1 day</option>
|
||||||
<option selected value="">never</option>
|
<option value="7">7 days</option>
|
||||||
</select>
|
<option value="14">14 days</option>
|
||||||
</p>
|
<option value="30">30 days</option>
|
||||||
<p class="dragregion">Click the file to copy the url</p>
|
<option selected value="">never</option>
|
||||||
</div>
|
</select>
|
||||||
</form>
|
</p>
|
||||||
<section class="main">
|
<p class="dragregion">Click the file to copy the url</p>
|
||||||
<ul class="embedder-list">
|
</div>
|
||||||
<% if (files && files.length > 0) { %>
|
</form>
|
||||||
<%- include('partials/_fileList.ejs',) %>
|
<section class="main">
|
||||||
<% } %>
|
<ul id="embedder-list" class="embedder-list" hx-get="/media-list" hx-trigger="load"></ul>
|
||||||
</ul>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
<footer class="info">
|
||||||
<footer class="info">
|
<p><a href="https://l.nekomimi.pet/project">Created by Wavering Ana</a></p>
|
||||||
<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>
|
||||||
<p><a href="https://github.com/WaveringAna/Embedder">Github</a></p>
|
</footer>
|
||||||
</footer>
|
<script src="/js/index.js"></script>
|
||||||
<script src="/js/index.js"></script>
|
<script>
|
||||||
</body>
|
document.body.addEventListener('htmx:afterSettle', function(event) {
|
||||||
</html>
|
var swappedElement = event.target;
|
||||||
|
|
||||||
|
if (swappedElement.id === 'embedder-list' || swappedElement.closest('#embedder-list')) {
|
||||||
|
console.log('htmx:afterSwap', swappedElement.id);
|
||||||
|
files = JSON.parse('<%- JSON.stringify(files) %>');
|
||||||
|
refreshMediaList(files);
|
||||||
|
console.log(files);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -7,170 +7,61 @@ const videoExtensions = ['.mp4', '.mov', '.avi', '.flv', '.mkv', '.wmv', '.webm'
|
||||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.tiff', '.webp'];
|
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.tiff', '.webp'];
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<script>
|
|
||||||
let files = JSON.parse('<%- JSON.stringify(files) %>');
|
|
||||||
const videoExtensions = ['.mp4', '.mov', '.avi', '.flv', '.mkv', '.wmv', '.webm'];
|
|
||||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.tiff', '.webp'];
|
|
||||||
|
|
||||||
function extension(string) {
|
|
||||||
console.log(string)
|
|
||||||
const file = string.split("/").pop();
|
|
||||||
return [
|
|
||||||
file.substr(0, file.lastIndexOf(".")),
|
|
||||||
file.substr(file.lastIndexOf("."), file.length).toLowerCase(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(files)
|
|
||||||
files.forEach(file => {
|
|
||||||
if (videoExtensions.includes(extension(file.path)[1])) {
|
|
||||||
console.log(`Fetching /uploads/720p-${file.path}.processing`)
|
|
||||||
fetch(`/uploads/720p-${file.path}.processing`)
|
|
||||||
.then(response => {
|
|
||||||
if (response.ok) {
|
|
||||||
// Video is still processing
|
|
||||||
console.log(`File /uploads/720p-${file.path}.processing exists, starting check...`)
|
|
||||||
checkFileAvailability(file.path);
|
|
||||||
} else {
|
|
||||||
// Video done processing, display it immediately
|
|
||||||
console.log(`File /uploads/720p-${file.path}.processing no longer exists, displaying...`)
|
|
||||||
createVideoElement(file.path);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Error:', error));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function checkFileAvailability(filePath) {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
fetch(`/uploads/720p-${filePath}.processing`)
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
clearInterval(interval);
|
|
||||||
createVideoElement(filePath);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Error:', error));
|
|
||||||
}, 5000); // Check every 5 seconds
|
|
||||||
}
|
|
||||||
|
|
||||||
function createVideoElement(filePath) {
|
|
||||||
const videoContainer = document.getElementById(`video-${filePath}`);
|
|
||||||
videoContainer.innerHTML = `
|
|
||||||
<video class="image" autoplay loop muted playsinline loading="lazy">
|
|
||||||
<source src="/uploads/720p-${filePath}" loading="lazy">
|
|
||||||
</video>
|
|
||||||
`;
|
|
||||||
videoContainer.style.display = 'block';
|
|
||||||
document.getElementById(`spinner-${filePath}`).style.display = 'none';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.spinner {
|
|
||||||
/* Positioning and Sizing */
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
position: relative;
|
|
||||||
margin: 50px auto; /* Centering the spinner */
|
|
||||||
|
|
||||||
/* Text Styling */
|
|
||||||
color: #555;
|
|
||||||
text-align: center;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
padding-top: 80px; /* Adjust as needed for text position */
|
|
||||||
|
|
||||||
/* Adding a background to the spinner for better visibility */
|
|
||||||
background-color: rgba(255, 255, 255, 0.8);
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Keyframes for the spinner animation */
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Spinner Animation */
|
|
||||||
.spinner::before {
|
|
||||||
content: '';
|
|
||||||
box-sizing: border-box;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
width: 40px; /* Spinner Size */
|
|
||||||
height: 40px;
|
|
||||||
margin-top: -20px; /* Half of height */
|
|
||||||
margin-left: -20px; /* Half of width */
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
border-top-color: #007bff; /* Spinner Color */
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- _fileList.ejs -->
|
<!-- _fileList.ejs -->
|
||||||
<% files.forEach(function(file) { %>
|
<% files.forEach(function(file) { %>
|
||||||
<li id="<%= file.path %>" class="show">
|
<li id="<%= file.path %>" class="show">
|
||||||
<form action="<%= file.url %>" method="post">
|
<div class="view">
|
||||||
<div class="view">
|
<% if (videoExtensions.includes(extension(file.path))) { %>
|
||||||
<% if (videoExtensions.includes(extension(file.path))) { %>
|
<!-- Show spinner initially -->
|
||||||
<!-- Show spinner initially -->
|
<div id="spinner-<%= file.path %>" class="spinner">Optimizing Video for Sharing...</div>
|
||||||
<div id="spinner-<%= file.path %>" class="spinner">Optimizing Video for Sharing...</div>
|
|
||||||
|
|
||||||
<!-- Hidden video container to be displayed later -->
|
<!-- Hidden video container to be displayed later -->
|
||||||
<div class="video">
|
<div class="video">
|
||||||
<video id="video-<%= file.path %>" class="image" autoplay loop muted playsinline loading="lazy" style="display: none;">
|
<video id="video-<%= file.path %>" class="image" autoplay loop muted playsinline loading="lazy" style="display: none;">
|
||||||
<source src="/uploads/720p-<%= file.path %>" loading="lazy">
|
<source src="/uploads/720p-<%= file.path %>" loading="lazy">
|
||||||
</video>
|
</video>
|
||||||
<div class="overlay">
|
<div class="overlay">
|
||||||
<% if(user.username == "admin" && file.username != "admin") { %>
|
|
||||||
<small class="username"><%= file.username %></small>
|
|
||||||
<br>
|
|
||||||
<% } %>
|
|
||||||
<a href="/gifv/<%=file.path %>" onclick="copyA(event)">Copy as GIFv</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% } else if (extension(file.path) == ".gif") { %>
|
|
||||||
<div class="video">
|
|
||||||
<img class="image" src="/uploads/720p-<%=file.path %>" width="100%" onclick="copyURI(event);" loading="lazy">
|
|
||||||
<div class="overlay">
|
|
||||||
<% if(user.username == "admin" && file.username != "admin") { %>
|
|
||||||
<small class="username"><%= file.username %></small>
|
|
||||||
<br>
|
|
||||||
<% } %>
|
|
||||||
<a href="/gifv/<%=file.path %>" onclick="copyA(event)">Copy as GIFv</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% } else if (imageExtensions.includes(extension(file.path))) { %>
|
|
||||||
<div class="video">
|
|
||||||
<img class="image" src="/uploads/<%=file.path %>" width="100%" onclick="copyURI(event)" loading="lazy">
|
|
||||||
<% if(user.username == "admin" && file.username != "admin") { %>
|
<% if(user.username == "admin" && file.username != "admin") { %>
|
||||||
<div class="overlay">
|
<small class="username"><%= file.username %></small>
|
||||||
<small class="username"><%= file.username %></small>
|
<br>
|
||||||
</div>
|
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<a href="/gifv/<%=file.path %>" onclick="copyA(event)">Copy as GIFv</a>
|
||||||
</div>
|
</div>
|
||||||
<% } else {%>
|
</div>
|
||||||
<!-- non-media file -->
|
<% } else if (extension(file.path) == ".gif") { %>
|
||||||
<div class="nonmedia" onclick="copyPath('/uploads/<%=file.path%>')">
|
<div class="video">
|
||||||
<p><%=extension(file.path)%> file</p>
|
<img class="image" src="/uploads/720p-<%=file.path %>" width="100%" onclick="copyURI(event);" loading="lazy">
|
||||||
|
<div class="overlay">
|
||||||
<% if(user.username == "admin" && file.username != "admin") { %>
|
<% if(user.username == "admin" && file.username != "admin") { %>
|
||||||
<div class="overlay">
|
<small class="username"><%= file.username %></small>
|
||||||
<small class="username"><%= file.username %></small>
|
<br>
|
||||||
</div>
|
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<a href="/gifv/<%=file.path %>" onclick="copyA(event)">Copy as GIFv</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } else if (imageExtensions.includes(extension(file.path))) { %>
|
||||||
|
<div class="video">
|
||||||
|
<img class="image" src="/uploads/<%=file.path %>" width="100%" onclick="copyURI(event)" loading="lazy">
|
||||||
|
<% if(user.username == "admin" && file.username != "admin") { %>
|
||||||
|
<div class="overlay">
|
||||||
|
<small class="username"><%= file.username %></small>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
<label><%= file.path %></label>
|
|
||||||
<button class="destroy" form="delete-<%= file.path %>"></button>
|
|
||||||
<button type="button" class="fullsize" onclick="openFullSize('/uploads/<%=file.path%>')"></button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<% } else {%>
|
||||||
<form name="delete-<%= file.path %>" id="delete-<%= file.path %>" action="<%= file.url %>/delete" method="post">
|
<!-- non-media file -->
|
||||||
</form>
|
<div class="nonmedia" onclick="copyPath('/uploads/<%=file.path%>')">
|
||||||
|
<p><%=extension(file.path)%> file</p>
|
||||||
|
<% if(user.username == "admin" && file.username != "admin") { %>
|
||||||
|
<div class="overlay">
|
||||||
|
<small class="username"><%= file.username %></small>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
<label><%= file.path %></label>
|
||||||
|
<button class="destroy" hx-get="<%=file.url%>/delete" hx-trigger="click" hx-target="#embedder-list" hx-swap="innerHTML"></button>
|
||||||
|
<button type="button" class="fullsize" onclick="openFullSize('/uploads/<%=file.path%>')"></button>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<% }); %>
|
<% }); %>
|
BIN
bun.lockb
Executable file
BIN
bun.lockb
Executable file
Binary file not shown.
12
docker/bun-Dockerfile
Normal file
12
docker/bun-Dockerfile
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
FROM oven/bun:alpine
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY ./tsconfig.json /usr/src/app
|
||||||
|
COPY ./package*.json /usr/src/app/
|
||||||
|
COPY ./bun.lockb /usr/src/app
|
||||||
|
COPY ./app/ /usr/src/app/app
|
||||||
|
|
||||||
|
RUN bun install
|
||||||
|
|
||||||
|
CMD ["bun", "start"]
|
Loading…
Add table
Add a link
Reference in a new issue