diff --git a/app/app.ts b/app/app.ts index a160a43..587680e 100644 --- a/app/app.ts +++ b/app/app.ts @@ -18,7 +18,7 @@ import indexRouter from "./routes/index"; import adduserRouter from "./routes/adduser"; import settingsRouter from "./routes/settings"; -import {db, expire, createDatabase, updateDatabase, MediaRow} from "./lib/db"; +import { db, expire, createDatabase, updateDatabase, MediaRow } from "./lib/db"; const app = express(); const server = http.createServer(app); @@ -70,11 +70,11 @@ function onError(error: any) { // Check if there is an existing DB or not, then check if it needs to be updated to new schema db.get("SELECT * FROM sqlite_master WHERE name ='users' and type='table'", async (err, row) => { - if (!row) createDatabase(3); + if (!row) createDatabase(3); else checkVersion(); }); -function checkVersion () { +function checkVersion() { db.get("PRAGMA user_version", (err: Error, row: any) => { if (row && row.user_version) { const version = row.user_version; @@ -128,7 +128,7 @@ app.use("/", settingsRouter); app.use("/uploads", express.static("uploads")); -async function prune () { +async function prune() { db.all("SELECT * FROM media", (err: Error, rows: []) => { console.log("Uploaded files: " + rows.length); console.log(rows); diff --git a/app/lib/middleware.ts b/app/lib/middleware.ts index 3aa21f7..a5140c3 100644 --- a/app/lib/middleware.ts +++ b/app/lib/middleware.ts @@ -6,6 +6,7 @@ import process from "process"; import { extension, videoExtensions, imageExtensions, oembedObj } from "./lib"; import { insertToDB } from "./db"; import { ffmpegDownscale, ffProbe } from "./ffmpeg"; +import { MediaProcessor } from "../services/MediaProcesser"; export const checkAuth: Middleware = (req, res, next) => { if (!req.user) { @@ -17,7 +18,7 @@ export const checkAuth: Middleware = (req, res, next) => { /**Checks shareX auth key */ export const checkSharexAuth: Middleware = (req, res, next) => { const auth = - process.env.EBAPI_KEY || process.env.EBPASS || "pleaseSetAPI_KEY"; + process.env.EBAPI_KEY || process.env.EBPASS || "pleaseSetAPI_KEY"; let key = null; if (req.headers["key"]) { @@ -57,8 +58,8 @@ export const createEmbedData: Middleware = async (req, res, next) => { for (const file in files) { const [filename, fileExtension] = extension(files[file].filename); const isMedia = - videoExtensions.includes(fileExtension) || - imageExtensions.includes(fileExtension); + videoExtensions.includes(fileExtension) || + imageExtensions.includes(fileExtension); const oembed: oembedObj = { type: "video", @@ -166,3 +167,25 @@ export const handleUpload: Middleware = async (req, res, next) => { res.status(500).send("Error processing files."); } }; + +export const processUploadedMedia: Middleware = async (req, res, next) => { + try { + const files = req.files as Express.Multer.File[]; + + for (const file of files) { + const [filename, fileExtension] = extension(file.filename); + + if (videoExtensions.includes(fileExtension)) { + MediaProcessor.processVideo( + file.path, + filename, + fileExtension + ).catch(err => console.error("Error processing video:", err)); + } + } + + next(); + } catch (error) { + next(error); + } +}; \ No newline at end of file diff --git a/app/lib/ws.ts b/app/lib/ws.ts deleted file mode 100644 index ed28975..0000000 --- a/app/lib/ws.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { EventEmitter } from "events"; - -import WebSocket from "ws"; - -const eventEmitter = new EventEmitter(); - -const wsPort = normalizePort(process.env.EBWSPORT || "3001"); - -const clients: WebSocket[] = []; - -/** - * Normalizes a port number to ensure it is a valid integer. - * - * @param {string} val - The port number as a string. - * @returns {number} The normalized port number. - */ -function normalizePort(val: string) { - const port = parseInt(val, 10); - - if (isNaN(port)) { - return parseInt(val); - } - - if (port >= 0) { - return port; - } -} -/** - * The WebSocket server instance. - */ -const wss = new WebSocket.Server({port: wsPort}); - -wss.on("connection", (ws) => { - clients.push(ws); - - ws.on("message", handleMessage); - - ws.on("close", handleMessage); - - ws.on("error", handleMessage); - - ws.on("close", () => { - const index = clients.indexOf(ws); - if (index !== -1) { - clients.splice(index, 1); - } - }); -}); - -/** - * Handles incoming messages from clients. - * - * @param {string} message - The incoming message. - */ -function handleMessage(message: string) { - try { - const data = JSON.parse(message); - - switch (data.type) { - case "message": - eventEmitter.emit("message", data.message); - break; - case "close": - eventEmitter.emit("close", data.userId); - break; - case "error": - eventEmitter.emit("error", data.error); - break; - default: - console.log(`Unknown message type: ${data.type}`); - } - } catch (error) { - console.log(`Error parsing message: ${error}`); - } -} - -/** - * Broadcasts a message to all connected clients. - * - * @param {string} message - The message to broadcast. - */ -function broadcast(message: string) { - wss.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(message); - } - }); -} - -/** - * Returns an array of all connected clients. - * - * @returns {WebSocket[]} An array of connected clients. - */ -function getClients() { - return clients; -} - -/** - * Sends a message to a specific client. - * - * @param {string} clientId - The ID of the client to send the message to. - * @param {string} message - The message to send. - */ -/*function sendMessageToClient(clientId: string, message: string) { - const client = clients.find((client) => client.id === clientId); - if (client) { - client.send(message); - } -}*/ - - - -//export { wss, eventEmitter, broadcast, getClients, sendMessageToClient }; - diff --git a/app/public/css/app.css b/app/public/css/app.css index a9f550d..3b836bb 100644 --- a/app/public/css/app.css +++ b/app/public/css/app.css @@ -27,13 +27,11 @@ height: 100px; position: relative; margin: 50px auto; - color: #555; text-align: center; font-family: Arial, sans-serif; font-size: 14px; padding-top: 80px; - background-color: rgba(255, 255, 255, 0.8); border-radius: 10px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); @@ -80,6 +78,7 @@ color: rgb(247, 248, 248); appearance: none; transition: border 0.15s ease 0s; + :focus { outline: none; box-shadow: none; @@ -231,4 +230,4 @@ label { .main { padding-top: 10px; -} +} \ No newline at end of file diff --git a/app/public/js/index.js b/app/public/js/index.js index 8fdf11f..ffb993a 100644 --- a/app/public/js/index.js +++ b/app/public/js/index.js @@ -2,294 +2,255 @@ /* eslint-disable no-undef */ /* eslint-env browser: true */ -let newMediaList; +const MEDIA_TYPES = { + video: ['.mp4', '.mov', '.avi', '.flv', '.mkv', '.wmv', '.webm'], + image: ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.tiff', '.webp'] +}; -const videoExtensions = [ - ".mp4", - ".mov", - ".avi", - ".flv", - ".mkv", - ".wmv", - ".webm", -]; +const getFileExtension = filename => { + const parts = filename.split('.'); + return parts.length > 1 ? `.${parts.pop().toLowerCase()}` : ''; +}; -const imageExtensions = [ - ".jpg", - ".jpeg", - ".png", - ".gif", - ".bmp", - ".svg", - ".tiff", - ".webp", -]; +const getMediaType = filename => { + const ext = getFileExtension(filename); + if (MEDIA_TYPES.video.includes(ext)) return 'video'; + if (MEDIA_TYPES.image.includes(ext)) return 'image'; + return 'other'; +}; +class FileUploader { + constructor() { + this.dropArea = document.getElementById('dropArea'); + this.gallery = document.getElementById('gallery'); + this.setupEventListeners(); + this.setupProgressUpdates(); + } -function copyURI(evt) { - evt.preventDefault(); - navigator.clipboard - .writeText(absolutePath(evt.target.getAttribute("src"))) - .then( - () => { - console.log("copied"); - }, - () => { - console.log("failed"); + setupEventListeners() { + // Drag and drop handlers + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + this.dropArea.addEventListener(eventName, e => { + e.preventDefault(); + e.stopPropagation(); + }); + }); + + ['dragenter', 'dragover'].forEach(eventName => { + this.dropArea.addEventListener(eventName, () => + this.dropArea.classList.add('highlight')); + }); + + ['dragleave', 'drop'].forEach(eventName => { + this.dropArea.addEventListener(eventName, () => + this.dropArea.classList.remove('highlight')); + }); + + // Handle file drops + this.dropArea.addEventListener('drop', e => + this.handleFiles(e.dataTransfer.files)); + + // Handle paste events + window.addEventListener('paste', e => this.handlePaste(e)); + + // Handle manual file selection + document.getElementById('fileupload') + .addEventListener('change', e => this.handleFiles(e.target.files)); + + // Handle manual upload button + document.getElementById('submit') + .addEventListener('click', () => this.uploadSelectedFiles()); + } + + setupProgressUpdates() { + console.log("Setting up SSE connection..."); + const evtSource = new EventSource('/progress-updates'); + + evtSource.onopen = () => { + console.log("SSE connection established"); + }; + + evtSource.onmessage = event => { + console.log("Raw SSE data:", event.data); + const data = JSON.parse(event.data); + + if (data.type === 'connected') { + console.log("Initial connection established"); + return; } - ); -} -function copyA(evt) { - evt.preventDefault(); - navigator.clipboard - .writeText(absolutePath(evt.target.getAttribute("href"))) - .then( - () => { - console.log("copied"); - }, - () => { - console.log("failed"); + const { filename, progress, status } = data; + const sanitizedFilename = sanitizeId(filename); + + console.log("Looking for elements:", { + spinnerSelector: `spinner-${sanitizedFilename}`, + containerSelector: `media-container-${sanitizedFilename}`, + }); + + const spinnerElement = document.getElementById(`spinner-${sanitizedFilename}`); + const containerElement = document.getElementById(`media-container-${sanitizedFilename}`); + + if (!spinnerElement || !containerElement) { + console.warn("Could not find required elements for:", filename); + return; } - ); -} -function copyPath(evt) { - navigator.clipboard.writeText(absolutePath(evt)).then( - () => { - console.log("copied"); - }, - () => { - console.log("failed"); + if (status === 'complete') { + console.log("Processing complete, showing video for:", filename); + spinnerElement.style.display = 'none'; + containerElement.style.display = 'block'; + } else if (status === 'processing') { + console.log("Updating progress for:", filename); + spinnerElement.textContent = + `Optimizing Video for Sharing: ${(progress * 100).toFixed(2)}% done`; + } + }; + + evtSource.onerror = (err) => { + console.error("SSE Error:", err); + }; + } + + async handleFiles(files) { + const filesArray = [...files]; + for (const file of filesArray) { + await this.uploadFile(file); } - ); + } + + handlePaste(e) { + const items = [...e.clipboardData.items] + .filter(item => item.type.indexOf('image') !== -1); + + if (items.length) { + const file = items[0].getAsFile(); + this.uploadFile(file); + } + } + + async uploadFile(file) { + const formData = new FormData(); + formData.append('fileupload', file); + formData.append('expire', document.getElementById('expire').value); + + try { + const response = await fetch('/', { + method: 'POST', + body: formData + }); + + if (!response.ok) throw new Error(`Upload failed: ${response.status}`); + + // Get the new file list HTML and insert it + const listResponse = await fetch('/media-list'); + const html = await listResponse.text(); + document.getElementById('embedder-list').innerHTML = html; + + // Clear preview + this.gallery.innerHTML = ''; + + } catch (error) { + console.error('Upload error:', error); + alert('Upload failed: ' + error.message); + } + } + + uploadSelectedFiles() { + const fileInput = document.getElementById('fileupload'); + if (fileInput.files.length) { + this.handleFiles(fileInput.files); + } + } + + showMediaElement(filename) { + const container = document.getElementById(`media-container-${filename}`); + const spinner = document.getElementById(`spinner-${filename}`); + + if (container && spinner) { + const mediaType = getMediaType(filename); + + if (mediaType === 'video') { + container.innerHTML = ` + `; + } + + spinner.style.display = 'none'; + container.style.display = 'block'; + } + } } -function absolutePath(href) { - let link = document.createElement("a"); +// Initialize on page load +document.addEventListener('DOMContentLoaded', () => { + new FileUploader(); + + // Setup search functionality + const searchInput = document.getElementById('search'); + if (searchInput) { + searchInput.addEventListener('input', e => { + const searchValue = e.target.value.toLowerCase(); + const mediaItems = document.querySelectorAll('ul.embedder-list li'); + + mediaItems.forEach(item => { + const matches = item.id.toLowerCase().includes(searchValue); + item.classList.toggle('hide', !matches); + item.classList.toggle('show', matches); + + item.addEventListener('animationend', function handler() { + if (!matches && searchValue !== '') { + this.style.display = 'none'; + } + this.removeEventListener('animationend', handler); + }); + }); + }); + } +}); + +// Utility functions for the media list +window.copyURI = e => { + e.preventDefault(); + navigator.clipboard.writeText(absolutePath(e.target.getAttribute('src'))) + .then(() => console.log('Copied to clipboard')) + .catch(err => console.error('Copy failed:', err)); +}; + +window.copyA = e => { + e.preventDefault(); + navigator.clipboard.writeText(absolutePath(e.target.getAttribute('href'))) + .then(() => console.log('Copied to clipboard')) + .catch(err => console.error('Copy failed:', err)); +}; + +window.copyPath = evt => { + navigator.clipboard.writeText(absolutePath(evt)) + .then(() => console.log('Copied to clipboard')) + .catch(err => console.error('Copy failed:', err)); +}; + +window.absolutePath = href => { + const link = document.createElement('a'); link.href = href; return link.href; -} +}; -function extension(string) { - return string.slice(((string.lastIndexOf(".") - 2) >>> 0) + 2); -} +window.openFullSize = imageUrl => { + const modal = document.createElement('div'); + modal.className = 'modal'; -let dropArea = document.getElementById("dropArea"); + const mediaType = getMediaType(imageUrl); + const element = mediaType === 'video' + ? `` + : ``; -["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => { - dropArea.addEventListener(eventName, preventDefaults, false); -}); - -function preventDefaults(e) { - e.preventDefault(); - e.stopPropagation(); -} - -["dragenter", "dragover"].forEach((eventName) => { - dropArea.addEventListener(eventName, highlight, false); -}); -["dragleave", "drop"].forEach((eventName) => { - dropArea.addEventListener(eventName, unhighlight, false); -}); - -function highlight(e) { - dropArea.classList.add("highlight"); -} - -function unhighlight(e) { - dropArea.classList.remove("highlight"); -} - -dropArea.addEventListener("drop", handleDrop, false); -window.addEventListener("paste", handlePaste); - -function handleDrop(e) { - let dt = e.dataTransfer; - let files = dt.files; - handleFiles(files); -} - -function handlePaste(e) { - // Get the data of clipboard - const clipboardItems = e.clipboardData.items; - const items = [].slice.call(clipboardItems).filter(function (item) { - // Filter the image items only - return item.type.indexOf("image") !== -1; - }); - if (items.length === 0) { - return; - } - - const item = items[0]; - // Get the blob of image - const blob = item.getAsFile(); - console.log(blob); - - uploadFile(blob); - previewFile(blob); -} - -function handleFiles(files) { - files = [...files]; - files.forEach(uploadFile); - files.forEach(previewFile); -} - -function previewFile(file) { - let reader = new FileReader(); - reader.readAsDataURL(file); - reader.onloadend = function () { - let img = document.createElement("img"); - img.src = reader.result; - img.className = "image"; - document.getElementById("gallery").appendChild(img); - document.getElementById("fileupload").src = img.src; - }; -} - -function uploadFile(file) { - let xhr = new XMLHttpRequest(); - let formData = new FormData(); - - xhr.open("POST", "/", true); - - xhr.addEventListener("readystatechange", function () { - if (xhr.readyState == 4) { - if (xhr.status == 200) { - //document.getElementById("embedder-list").innerHTML = response; - 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}`); - } - } - }); - - if (file == null || file == undefined) { - file = document.querySelector("#fileupload").files[0]; - } - - formData.append("fileupload", file); - formData.append("expire", document.getElementById("expire").value); - xhr.send(formData); -} - -function openFullSize(imageUrl) { - let modal = document.createElement("div"); - modal.classList.add("modal"); - let img = document.createElement("img"); - let video = document.createElement("video"); - img.src = imageUrl; - video.src = imageUrl; - video.controls = true; - - if ( - imageExtensions.includes(extension(imageUrl)) - ) { - modal.appendChild(img); - } else if ( - videoExtensions.includes(extension(imageUrl)) - ) { - modal.appendChild(video); - } - - // Add the modal to the page + modal.innerHTML = element; document.body.appendChild(modal); - // Add an event listener to close the modal when the user clicks on it - modal.addEventListener("click", function () { - modal.remove(); - }); -} + modal.addEventListener('click', () => modal.remove()); +}; -let searchInput = document.getElementById("search"); - -searchInput.addEventListener("input", () => { - let searchValue = searchInput.value; - let mediaList = document.querySelectorAll("ul.embedder-list li"); - - mediaList.forEach((li) => { - if (!li.id.toLowerCase().includes(searchValue)) { - //make lowercase to allow case insensitivity - li.classList.add("hide"); - li.classList.remove("show"); - li.addEventListener( - "animationend", - function () { - if (searchInput.value !== "") { - this.style.display = "none"; - } - }, - { once: true } - ); // The {once: true} option automatically removes the event listener after it has been called - } else { - li.style.display = ""; - li.classList.remove("hide"); - if (searchValue === "" && !li.classList.contains("show")) { - li.classList.add("show"); - } - } - }); -}); - -function p(num) { - return `${(num * 100).toFixed(2)}%`; -} - -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().then(json => { - document.getElementById(`spinner-${filePath}`).innerText = `Optimizing Video for Sharing: ${p(json.progress)} done`; - return response; - }); - } else if (response.status === 404) { - console.log(`${filePath} finished processing`); - console.log(`/uploads/720p-${filePath}-progress.json finished`); - clearInterval(interval); - createVideoElement(filePath); - } else { - throw new Error(`HTTP error: Status code ${response.status}`); - } - }) - .catch((error) => console.error("Error:", error)); - }; - - checkFile(); - const interval = setInterval(checkFile, 1000); -} - -function createVideoElement(filePath) { - const videoContainer = document.getElementById(`video-${filePath}`); - videoContainer.outerHTML = ` - - `; - videoContainer.style.display = "block"; - document.getElementById(`spinner-${filePath}`).style.display = "none"; -} - -function updateMediaList() { - htmx.ajax("GET", "/media-list", {target: "#embedder-list", swap: "innerHTML"}); - htmx.process(document.body); -} - -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...`); - } - }); -} +function sanitizeId(filename) { + return filename.replace(/[^a-z0-9]/gi, '_'); +} \ No newline at end of file diff --git a/app/routes/index.ts b/app/routes/index.ts index e1d9ca5..7a0bf26 100644 --- a/app/routes/index.ts +++ b/app/routes/index.ts @@ -17,12 +17,13 @@ import path from "path"; import { extension, videoExtensions, oembedObj } from "../lib/lib"; import { db, MediaRow, getPath, deleteId } from "../lib/db"; import { fileStorage } from "../lib/multer"; +import { progressManager } from "../services/ProgressManager"; import { checkAuth, checkSharexAuth, - convertTo720p, createEmbedData, handleUpload, + processUploadedMedia, } from "../lib/middleware"; const upload = multer({ storage: fileStorage /**, fileFilter: fileFilter**/ }); //maybe make this a env variable? @@ -30,9 +31,8 @@ const upload = multer({ storage: fileStorage /**, fileFilter: fileFilter**/ }); const fetchMedia: Middleware = (req, res, next) => { const admin: boolean = req.user.username == "admin" ? true : false; - /**Check if the user is an admin, if so, show all posts from all users */ - const query: string = admin - ? "SELECT * FROM media" + const query: string = admin + ? "SELECT * FROM media" : "SELECT * FROM media WHERE username = ?"; const params: any[] = admin ? [] : [req.user.username]; @@ -43,15 +43,20 @@ const fetchMedia: Middleware = (req, res, next) => { return res.status(500).send("Database error"); } const files = rows.map((row: MediaRow) => { + const isProcessed = videoExtensions.includes(extension(row.path)[1]) ? + fs.existsSync(`uploads/720p-${row.path}`) : + true; + return { id: row.id, path: row.path, expire: row.expire, username: row.username, url: "/" + row.id, + isProcessed }; }); - res.locals.files = files.reverse(); //reverse so newest files appear first + res.locals.files = files.reverse(); res.locals.Count = files.length; next(); }); @@ -59,6 +64,29 @@ const fetchMedia: Middleware = (req, res, next) => { const router = express.Router(); +router.get('/progress-updates', (req, res) => { + console.log("SSE connection requested"); // Debug log + + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + + // Send an initial message to confirm connection + res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`); + + const sendUpdate = (data: any) => { + res.write(`data: ${JSON.stringify(data)}\n\n`); + }; + + progressManager.subscribeToUpdates(sendUpdate); + + // Clean up on client disconnect + req.on('close', () => { + console.log("SSE connection closed"); // Debug log + progressManager.unsubscribeFromUpdates(sendUpdate); + }); +}); + router.get( "/", (req: Request, res: Response, next: NextFunction) => { @@ -79,18 +107,17 @@ router.get("/media-list", fetchMedia, (req: Request, res: Response) => { router.get( "/gifv/:file", async (req: Request, res: Response, next: NextFunction) => { - const url = `${req.protocol}://${req.get("host")}/uploads/${ - req.params.file - }`; + const url = `${req.protocol}://${req.get("host")}/uploads/${req.params.file + }`; let width; let height; const [filename, fileExtension] = extension(`uploads/${req.params.file}`); if ( fileExtension == ".mp4" || - fileExtension == ".mov" || - fileExtension == ".webm" || - fileExtension == ".gif" + fileExtension == ".mov" || + fileExtension == ".webm" || + fileExtension == ".gif" ) { const imageData = ffProbe( `uploads/${req.params.file}`, @@ -121,7 +148,7 @@ router.get( } ); -router.get("/oembed/:file", +router.get("/oembed/:file", async (req: Request, res: Response) => { const filename = req.params.file; const fileExtension = filename.slice(filename.lastIndexOf(".")); @@ -142,7 +169,7 @@ router.get("/oembed/:file", const ffprobeData = await ffProbe(`uploads/${filename}`, filename, fileExtension); oembedData.width = ffprobeData.streams[0].width; oembedData.height = ffprobeData.streams[0].height; - + oembedData.html = ``; } else { const imageData = await imageProbe(fs.createReadStream(`uploads/${filename}`)); @@ -167,10 +194,10 @@ router.post( [ checkAuth, upload.array("fileupload"), - convertTo720p, - createEmbedData, handleUpload, fetchMedia, + processUploadedMedia, + createEmbedData, ], (req: Request, res: Response) => { return res.render("partials/_fileList", { user: req.user }); // Render only the file list partial @@ -192,9 +219,9 @@ router.get( [checkAuth], async (req: Request, res: Response, next: NextFunction) => { const filename: any = await getPath(req.params.id); - const filePath = path.join(__dirname , "../../uploads/" + filename.path); + const filePath = path.join(__dirname, "../../uploads/" + filename.path); const oembed = path.join( - __dirname , "../../uploads/oembed-" + filename.path + ".json" + __dirname, "../../uploads/oembed-" + filename.path + ".json" ); const [fileName, fileExtension] = extension(filePath); @@ -202,10 +229,10 @@ router.get( if ( videoExtensions.includes(fileExtension) || - fileExtension == ".gif" + fileExtension == ".gif" ) { filesToDelete.push( - path.join(__dirname , "../../uploads/720p-" + filename.path) + path.join(__dirname, "../../uploads/720p-" + filename.path) ); } diff --git a/app/services/MediaProcesser.ts b/app/services/MediaProcesser.ts new file mode 100644 index 0000000..b380b92 --- /dev/null +++ b/app/services/MediaProcesser.ts @@ -0,0 +1,56 @@ +import ffmpeg from 'fluent-ffmpeg'; +import { progressManager } from './ProgressManager'; +import { EncodingType, currentEncoding } from '../lib/ffmpeg'; + +export class MediaProcessor { + static async processVideo( + inputPath: string, + filename: string, + extension: string + ): Promise { + console.log("Starting video processing:", filename); // Debug log + + const outputPath = `uploads/720p-${filename}${extension}`; + const outputOptions = [ + '-vf', 'scale=-2:720', + '-c:v', currentEncoding, + '-c:a', 'copy', + '-pix_fmt', 'yuv420p' + ]; + + return new Promise((resolve, reject) => { + ffmpeg() + .input(inputPath) + .outputOptions(outputOptions) + .output(outputPath) + .on('progress', (progress) => { + console.log("Progress:", progress.percent); // Debug log + progressManager.updateProgress({ + filename: `${filename}${extension}`, + progress: progress.percent / 100, + status: 'processing' + }); + }) + .on('end', () => { + console.log("Processing complete:", filename); // Debug log + progressManager.updateProgress({ + filename: `${filename}${extension}`, + progress: 1, + status: 'complete' + }); + resolve(); + }) + .on('error', (err) => { + console.error("Processing error:", err); // Debug log + progressManager.updateProgress({ + filename: `${filename}${extension}`, + progress: 0, + status: 'error', + message: err.message + }); + reject(err); + }) + .run(); + }); + } +} \ No newline at end of file diff --git a/app/services/ProgressManager.ts b/app/services/ProgressManager.ts new file mode 100644 index 0000000..1c9d9f1 --- /dev/null +++ b/app/services/ProgressManager.ts @@ -0,0 +1,45 @@ +import { EventEmitter } from 'events'; + +export interface ProgressUpdate { + filename: string; + progress: number; + status: 'processing' | 'complete' | 'error'; + message?: string; +} + +class ProgressManager { + private static instance: ProgressManager; + private emitter: EventEmitter; + private activeJobs: Map; + + private constructor() { + this.emitter = new EventEmitter(); + this.activeJobs = new Map(); + } + + static getInstance(): ProgressManager { + if (!ProgressManager.instance) { + ProgressManager.instance = new ProgressManager(); + } + return ProgressManager.instance; + } + + updateProgress(update: ProgressUpdate) { + this.activeJobs.set(update.filename, update); + this.emitter.emit('progress', update); + } + + subscribeToUpdates(callback: (update: ProgressUpdate) => void) { + this.emitter.on('progress', callback); + } + + unsubscribeFromUpdates(callback: (update: ProgressUpdate) => void) { + this.emitter.off('progress', callback); + } + + getJobStatus(filename: string): ProgressUpdate | undefined { + return this.activeJobs.get(filename); + } +} + +export const progressManager = ProgressManager.getInstance(); diff --git a/app/views/partials/_fileList.ejs b/app/views/partials/_fileList.ejs index 08577bc..4761756 100644 --- a/app/views/partials/_fileList.ejs +++ b/app/views/partials/_fileList.ejs @@ -1,73 +1,86 @@ <% -function extension(string) { -return string.slice((string.lastIndexOf(".") - 2 >>> 0) + 2); -} +const getMediaType = (filename) => { + const ext = filename.slice(((filename.lastIndexOf(".") - 2) >>> 0) + 2).toLowerCase(); + const videoExts = ['.mp4', '.mov', '.avi', '.flv', '.mkv', '.wmv', '.webm']; + const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.tiff', '.webp']; + + if (videoExts.includes(ext)) return 'video'; + if (imageExts.includes(ext)) return 'image'; + return 'other'; +}; -const videoExtensions = ['.mp4', '.mov', '.avi', '.flv', '.mkv', '.wmv', '.webm']; -const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.tiff', '.webp']; +function sanitizeId(filename) { + return filename.replace(/[^a-z0-9]/gi, '_'); +} %> - - - <% files.forEach(function(file) { %> -
  • -
    - <% if (videoExtensions.includes(extension(file.path))) { %> - -
    Optimizing Video for Sharing...
    - - -
    - -
    - <% if(user.username == "admin" && file.username != "admin") { %> - <%= file.username %> -
    - <% } %> - Copy as GIFv -
    -
    - <% } else if (extension(file.path) == ".gif") { %> -
    - -
    - <% if(user.username == "admin" && file.username != "admin") { %> - <%= file.username %> -
    - <% } %> - Copy as GIFv -
    -
    - <% } else if (imageExtensions.includes(extension(file.path))) { %> -
    - - <% if(user.username == "admin" && file.username != "admin") { %> -
    - <%= file.username %> -
    - <% } %> -
    - <% } else {%> - -
    -

    <%=extension(file.path)%> file

    - <% if(user.username == "admin" && file.username != "admin") { %> -
    - <%= file.username %> -
    - <% } %> -
    - <% } %> - - - -
    -
  • +
  • +
    + <% const mediaType = getMediaType(file.path); %> + + <% if (mediaType === 'video') { %> +
    + <% const sanitizedId = file.path.replace(/[^a-z0-9]/gi, '_'); %> + +
    +
    + Optimizing Video for Sharing... +
    +
    + +
    + +
    + +
    + <% if(user.username === "admin" && file.username !== "admin") { %> + <%= file.username %> +
    + <% } %> + Copy as GIFv +
    +
    + <% } else if (mediaType === 'image') { %> + +
    + + + <% if(user.username === "admin" && file.username !== "admin") { %> +
    + <%= file.username %> +
    + <% } %> +
    + + <% } else { %> + +
    +

    <%= file.path.split('.').pop().toUpperCase() %> file

    + <% if(user.username === "admin" && file.username !== "admin") { %> +
    + <%= file.username %> +
    + <% } %> +
    + <% } %> + + + + +
    +
  • <% }); %> \ No newline at end of file