htmx is so cool

This commit is contained in:
waveringana 2023-11-29 00:30:15 -05:00
parent e04ef78a42
commit 3ed7b0b5c7
11 changed files with 557 additions and 400 deletions

View file

@ -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;
} }

View file

@ -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);

View file

@ -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",

View file

@ -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.");
}
}; };

View file

@ -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);

View file

@ -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",
];

View file

@ -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;

View file

@ -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>

View file

@ -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

Binary file not shown.

12
docker/bun-Dockerfile Normal file
View 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"]