From e799724b3bee66eb4894ed5b3a2d6dd23647aab9 Mon Sep 17 00:00:00 2001 From: waveringana Date: Fri, 10 May 2024 02:43:52 -0400 Subject: [PATCH] add endpoint for oembed; restyle gifv; skeleton for websocket --- app/lib/ffmpeg.ts | 2 + app/lib/lib.ts | 4 +- app/lib/middleware.ts | 2 + app/lib/ws.ts | 20 +++ app/public/js/index.js | 388 ++++++++++++++++++++--------------------- app/routes/index.ts | 39 ++++- app/views/gifv.ejs | 29 +-- package-lock.json | 33 +++- package.json | 4 +- 9 files changed, 312 insertions(+), 209 deletions(-) create mode 100644 app/lib/ws.ts diff --git a/app/lib/ffmpeg.ts b/app/lib/ffmpeg.ts index 489dad7..3887a02 100644 --- a/app/lib/ffmpeg.ts +++ b/app/lib/ffmpeg.ts @@ -7,6 +7,8 @@ import which from "which"; import fs from "fs"; +import { wss } from "./ws"; + /** * Enum to represent different types of video encoding methods. * diff --git a/app/lib/lib.ts b/app/lib/lib.ts index 9322aef..7279357 100644 --- a/app/lib/lib.ts +++ b/app/lib/lib.ts @@ -31,7 +31,9 @@ export interface oembedObj { provider_name: string; provider_url: string; cache_age: number; + title: string; html: string; + url: string; width?: number; height?: number; } @@ -54,4 +56,4 @@ export const imageExtensions = [ ".svg", ".tiff", ".webp", -]; +]; \ No newline at end of file diff --git a/app/lib/middleware.ts b/app/lib/middleware.ts index bef0077..3aa21f7 100644 --- a/app/lib/middleware.ts +++ b/app/lib/middleware.ts @@ -69,6 +69,8 @@ export const createEmbedData: Middleware = async (req, res, next) => { html: ``, + title: filename, + url: `${req.protocol}://${req.get("host")}/uploads/${filename}${fileExtension}`, }; if (isMedia) { diff --git a/app/lib/ws.ts b/app/lib/ws.ts new file mode 100644 index 0000000..c672e24 --- /dev/null +++ b/app/lib/ws.ts @@ -0,0 +1,20 @@ +import WebSocket from "ws"; + +const wsPort = normalizePort(process.env.EBWSPORT || "3001"); + +function normalizePort(val: string) { + const port = parseInt(val, 10); + + if (isNaN(port)) { + return parseInt(val); + } + + if (port >= 0) { + return port; + } +} + +const wss = new WebSocket.Server({port: wsPort}); + +export { wss }; + diff --git a/app/public/js/index.js b/app/public/js/index.js index b015cd3..8fdf11f 100644 --- a/app/public/js/index.js +++ b/app/public/js/index.js @@ -1,295 +1,295 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-undef */ /* eslint-env browser: true */ let newMediaList; const videoExtensions = [ - ".mp4", - ".mov", - ".avi", - ".flv", - ".mkv", - ".wmv", - ".webm", + ".mp4", + ".mov", + ".avi", + ".flv", + ".mkv", + ".wmv", + ".webm", ]; const imageExtensions = [ - ".jpg", - ".jpeg", - ".png", - ".gif", - ".bmp", - ".svg", - ".tiff", - ".webp", + ".jpg", + ".jpeg", + ".png", + ".gif", + ".bmp", + ".svg", + ".tiff", + ".webp", ]; function copyURI(evt) { - evt.preventDefault(); - navigator.clipboard - .writeText(absolutePath(evt.target.getAttribute("src"))) - .then( - () => { - console.log("copied"); - }, - () => { - console.log("failed"); - } - ); + evt.preventDefault(); + navigator.clipboard + .writeText(absolutePath(evt.target.getAttribute("src"))) + .then( + () => { + console.log("copied"); + }, + () => { + console.log("failed"); + } + ); } function copyA(evt) { - evt.preventDefault(); - navigator.clipboard - .writeText(absolutePath(evt.target.getAttribute("href"))) - .then( - () => { - console.log("copied"); - }, - () => { - console.log("failed"); - } - ); + evt.preventDefault(); + navigator.clipboard + .writeText(absolutePath(evt.target.getAttribute("href"))) + .then( + () => { + console.log("copied"); + }, + () => { + console.log("failed"); + } + ); } function copyPath(evt) { - navigator.clipboard.writeText(absolutePath(evt)).then( - () => { - console.log("copied"); - }, - () => { - console.log("failed"); - } - ); + navigator.clipboard.writeText(absolutePath(evt)).then( + () => { + console.log("copied"); + }, + () => { + console.log("failed"); + } + ); } function absolutePath(href) { - let link = document.createElement("a"); - link.href = href; - return link.href; + let link = document.createElement("a"); + link.href = href; + return link.href; } function extension(string) { - return string.slice(((string.lastIndexOf(".") - 2) >>> 0) + 2); + return string.slice(((string.lastIndexOf(".") - 2) >>> 0) + 2); } let dropArea = document.getElementById("dropArea"); ["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => { - dropArea.addEventListener(eventName, preventDefaults, false); + dropArea.addEventListener(eventName, preventDefaults, false); }); function preventDefaults(e) { - e.preventDefault(); - e.stopPropagation(); + e.preventDefault(); + e.stopPropagation(); } ["dragenter", "dragover"].forEach((eventName) => { - dropArea.addEventListener(eventName, highlight, false); + dropArea.addEventListener(eventName, highlight, false); }); ["dragleave", "drop"].forEach((eventName) => { - dropArea.addEventListener(eventName, unhighlight, false); + dropArea.addEventListener(eventName, unhighlight, false); }); function highlight(e) { - dropArea.classList.add("highlight"); + dropArea.classList.add("highlight"); } function unhighlight(e) { - dropArea.classList.remove("highlight"); + 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); + 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) { + // 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; - } + 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); + const item = items[0]; + // Get the blob of image + const blob = item.getAsFile(); + console.log(blob); - uploadFile(blob); - previewFile(blob); + uploadFile(blob); + previewFile(blob); } function handleFiles(files) { - files = [...files]; - files.forEach(uploadFile); - files.forEach(previewFile); + 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; - }; + 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(); - let reader = new FileReader(); + let xhr = new XMLHttpRequest(); + let formData = new FormData(); - xhr.open("POST", "/", true); + xhr.open("POST", "/", true); - xhr.addEventListener("readystatechange", function (e) { - if (xhr.readyState == 4) { - if (xhr.status == 200) { - let response = xhr.responseText; - //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}`); - } + 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]; } - }); - 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); + 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; + 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); - } + if ( + imageExtensions.includes(extension(imageUrl)) + ) { + modal.appendChild(img); + } else if ( + videoExtensions.includes(extension(imageUrl)) + ) { + modal.appendChild(video); + } - // Add the modal to the page - document.body.appendChild(modal); + // Add the modal to the page + document.body.appendChild(modal); - // Add an event listener to close the modal when the user clicks on it - modal.addEventListener("click", function () { - modal.remove(); - }); + // Add an event listener to close the modal when the user clicks on it + modal.addEventListener("click", function () { + modal.remove(); + }); } let searchInput = document.getElementById("search"); searchInput.addEventListener("input", () => { - let searchValue = searchInput.value; - let mediaList = document.querySelectorAll("ul.embedder-list li"); + 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"); - } - } - }); + 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)}%`; + 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)); - }; + 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); + checkFile(); + const interval = setInterval(checkFile, 1000); } function createVideoElement(filePath) { - const videoContainer = document.getElementById(`video-${filePath}`); - videoContainer.outerHTML = ` + const videoContainer = document.getElementById(`video-${filePath}`); + videoContainer.outerHTML = ` `; - videoContainer.style.display = "block"; - document.getElementById(`spinner-${filePath}`).style.display = "none"; + 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); + 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...`); - } - }); + 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...`); + } + }); } diff --git a/app/routes/index.ts b/app/routes/index.ts index 9e796aa..084d48f 100644 --- a/app/routes/index.ts +++ b/app/routes/index.ts @@ -14,7 +14,7 @@ import { ffProbe } from "../lib/ffmpeg"; import fs from "fs"; import path from "path"; -import { extension, videoExtensions } from "../lib/lib"; +import { extension, videoExtensions, oembedObj } from "../lib/lib"; import { db, MediaRow, getPath, deleteId } from "../lib/db"; import { fileStorage } from "../lib/multer"; import { @@ -117,6 +117,43 @@ router.get( } ); +router.get("/oembed/:file", + async (req: Request, res: Response) => { + const filename = req.params.file; + const fileExtension = filename.slice(filename.lastIndexOf(".")); + + try { + const oembedData: oembedObj = { + type: (videoExtensions.includes(fileExtension) ? "photo" : "video"), + version: "1.0", + provider_name: "embedder", + provider_url: "https://github.com/WaveringAna/embedder", + cache_age: 86400, + title: filename.slice(0, filename.lastIndexOf(".")), + html: "", + url: `${req.protocol}://${req.get("host")}/uploads/${filename}` + }; + + if (videoExtensions.includes(fileExtension) || fileExtension === '.gif') { + const ffprobeData = await ffProbe(`uploads/${filename}`, filename, fileExtension); + oembedData.width = ffprobeData.streams[0].width; + oembedData.height = ffprobeData.streams[0].height; + + // Consider generating a thumbnail_url if it's a video + } else { + const imageData = await imageProbe(fs.createReadStream(`uploads/${filename}`)); + oembedData.width = imageData.width; + oembedData.height = imageData.height; + } + + res.json(oembedData); + } catch (error) { + console.error("Error generating oEmbed data:", error); + res.status(500).send("Error generating oEmbed data"); + } + } +); + router.post( "/", [ diff --git a/app/views/gifv.ejs b/app/views/gifv.ejs index 5607c2a..2e3e0e2 100644 --- a/app/views/gifv.ejs +++ b/app/views/gifv.ejs @@ -12,8 +12,9 @@ const videoExtensions = ['.mp4', '.mov', '.avi', '.flv', '.mkv', '.wmv', '.webm' <% if (extension(url)[1] == ".gif") { %> - - + + @@ -24,8 +25,9 @@ const videoExtensions = ['.mp4', '.mov', '.avi', '.flv', '.mkv', '.wmv', '.webm' <% } else if (videoExtensions.includes(extension(url)[1])) { %> - - + + @@ -36,8 +38,9 @@ const videoExtensions = ['.mp4', '.mov', '.avi', '.flv', '.mkv', '.wmv', '.webm' <% } else { %> - - + + @@ -109,11 +112,15 @@ footer a:hover { - <% if (videoExtensions.includes(extension(url)[1])) { %> - - <% } else { %> - - <% } %> +
+ <% if (videoExtensions.includes(extension(url)[1])) { %> + + <% } else { %> + <%= oembedData.title %> + <% } %> +