diff --git a/.gitignore b/.gitignore index f9a75c5..3190fb6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ videos/* - +!videos/.gitkeep +tmp/ diff --git a/index.js b/index.js index 498265a..1b0d56c 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,9 @@ import bcrypt from "bcrypt"; import fileUpload from "express-fileupload"; import path from "path"; import childProcess from "child_process"; +import levenshtein from "js-levenshtein"; +import cookieParser from "cookie-parser"; +import { fileURLToPath } from 'url'; const users = []; let sessions = []; @@ -13,14 +16,19 @@ function randomString(length) { const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789"; let result = ""; for (let i = 0; i < length; ++i) { - result += chars[chars.length * Math.random()]; + result += chars[Math.floor(chars.length * Math.random())]; } return result; } +function dirname() { + return path.dirname(fileURLToPath(import.meta.url)); +} + const app = express(); app.use(cors()); app.use(express.json()); +app.use(cookieParser()); app.use(express.urlencoded({ extended: true })); app.use("/", express.static("public/")); @@ -32,12 +40,12 @@ app.post("/api/register", async (req, res) => { return res.status(400).json({ ok: false, error: "bad request" }); } const existingUser = users.find(user => user.username === username); - if (existingUser === undefined) { + if (existingUser !== undefined) { return res.status(400).json({ ok: false, error: "username taken" }); } const passwordHash = await bcrypt.hash(password, 10); const id = users.length; - const user = { id, username, passwordHash }; + const user = { id, username, password: passwordHash }; users.push(user); return res.status(200).json({ ok: true, user }); }); @@ -63,10 +71,33 @@ app.post("/api/login", async (req, res) => { return res.status(200).json({ ok: true, session }); }); + +app.get("/api/search", async (req, res) => { + const page = +req.query.page || 0; + const search = req.query.query; + if (!search) { + return res.status(400).json({ ok: false, error: "bad request" }); + } + const [start, end] = [20 * page, 20 * (page + 1)]; + const withDistance = videos + .map(video => ({dist: levenshtein(search, video.title), ...video})); + withDistance.sort((a, b) => a.dist - b.dist); + const returnedVideos = withDistance + .slice(start, end) + .map(video => { + const user = users.find(user => user.id === video.userId); + if (!user) { + return {...video, author: "[Liberal]"}; + } + return {...video, author: user.username}; + }); + return res.status(200).json({ ok: true, videos: returnedVideos, total: videos.length }); +}); + function authorized() { return (req, res, next) => { const token = (() => { - if ("token" in req.cookies) { + if (req.cookies && "token" in req.cookies) { return req.cookies["token"]; } else if ("token" in req.query) { return req.query["token"]; @@ -77,13 +108,13 @@ function authorized() { } })(); if (token === null) { - return res.status(400).json({ ok: false, error: "unathorized" }); + return res.status(400).json({ ok: false, error: "unauthorized" }); } const session = sessions.find(session => session.token === token); if (session === undefined) { - return res.status(400).json({ ok: false, error: "unathorized" }); + return res.status(400).json({ ok: false, error: "unauthorized" }); } - const user = user.find(user => user.id === session.id); + const user = users.find(user => user.id === session.userId); if (user === undefined) { throw new Error("error: session with invalid userId"); } @@ -99,26 +130,27 @@ app.get("/api/logout", authorized(), (req, res) => { app.post("/api/upload_video", authorized(), fileUpload({ limits: { fileSize: 2 ** 26 }, useTempFiles: true }), async (req, res) => { const { title } = req.body; - if (req.files === undefined || req.files === null || req.files.length !== 1) { + if (!req.files || !req.files.video) { return res.status(400).json({ ok: false, error: "bad request" }); } - if (req.files[0].mimetype !== "video/mp4") { + if (req.files.video.mimetype !== "video/mp4") { return res.status(400).json({ ok: false, error: "bad mimetype" }); } const userId = req.user.id; const id = randomString(4); - const tempPath = req.files[0].tempFilePath; - const newPath = path.join("/videos", id, ".mp4"); + const tempPath = req.files.video.tempFilePath; + const newPath = path.join(dirname(), "videos", `${id}.mp4`); + console.log(newPath); const exitCode = await new Promise((resolve, _reject) => { - const process = childProcess.spawn("HandBrakeCLI", ["-i", tempPath, "-o", newPath, "-Z", "Social 25 MB 5 Minutes 360p60"]); + const process = childProcess.spawn("HandBrakeCLI", ["-i", tempPath, "-o", newPath, "-Z", "Social 50 MB 10 Minutes 480p30"]); process.stderr.on("data", (data) => { - conole.error(data); + console.error(data.toString()); }); process.on("close", (code) => { resolve(code); }) - }); + }) if (exitCode !== 0) { throw new Error("handbrake failed"); } diff --git a/package-lock.json b/package-lock.json index 0d304d4..0f269a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,11 @@ "license": "ISC", "dependencies": { "bcrypt": "^5.1.1", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", "express": "^4.18.2", - "express-fileupload": "^1.4.3" + "express-fileupload": "^1.4.3", + "js-levenshtein": "^1.1.6" } }, "node_modules/@mapbox/node-pre-gyp": { @@ -248,6 +250,26 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -668,6 +690,14 @@ "node": ">=8" } }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", diff --git a/package.json b/package.json index 75792e9..557fd78 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,10 @@ "type": "module", "dependencies": { "bcrypt": "^5.1.1", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", "express": "^4.18.2", - "express-fileupload": "^1.4.3" + "express-fileupload": "^1.4.3", + "js-levenshtein": "^1.1.6" } } diff --git a/public/header.js b/public/header.js new file mode 100644 index 0000000..90f7a86 --- /dev/null +++ b/public/header.js @@ -0,0 +1,37 @@ +function displayHeader() { + const links = [ + { + href: "/", + name: "Home", + }, + { + href: "/register/", + name: "Register", + }, + { + href: "/login/", + name: "Login", + }, + { + href: "/upload/", + name: "Upload", + }, + ].map(({name, href}) => { + if (href === window.location.pathname) { + return `${name}` + } else { + return `${name}` + } + }).join(" - "); + + document.querySelector("h1").outerHTML = ` +
+

MaoTube

+ +
+ ` +} + +displayHeader(); diff --git a/public/index.html b/public/index.html index 24c799b..02aed75 100644 --- a/public/index.html +++ b/public/index.html @@ -4,18 +4,19 @@ MaoTube -

MaoTube

-
- + + +

The chairman The chairman The chairman + diff --git a/public/login/index.html b/public/login/index.html new file mode 100644 index 0000000..934d7a0 --- /dev/null +++ b/public/login/index.html @@ -0,0 +1,36 @@ + + + + + MaoTube + + + +

MaoTube

+
+ + + + +
+
+ +
+
+
+ + +
+ + + + + diff --git a/public/login/script.js b/public/login/script.js new file mode 100644 index 0000000..f689adc --- /dev/null +++ b/public/login/script.js @@ -0,0 +1,46 @@ +function error(message) { + const errorContainer = document.getElementById("mao-error"); + const errorElement = document.getElementById("mao-error-message"); + + errorElement.innerText = message; + errorContainer.classList.remove("hidden"); +} + +function displayResponse(response) { + if (!response.ok) { + error(response.error); + return; + } + window.location.href = "/"; +} + +function click() { + const username = document.getElementById("username").value; + const password = document.getElementById("password").value; + + if (username.trim().length === 0) { + error("username cannot be empty"); + return; + } else if (password.trim().length === 0) { + error("password cannot be empty"); + return; + } + const method = "POST"; + const body = JSON.stringify({ username, password }); + const headers = new Headers({ "Content-Type": "application/json" }); + return fetch("/api/login", { + method, + body, + headers, + }) + .then(v => v.json()) + .then(displayResponse); + +} + +function main() { + const submit = document.getElementById("submit"); + submit.addEventListener("click", () => click()) +} + +main(); diff --git a/public/register/index.html b/public/register/index.html new file mode 100644 index 0000000..8b69018 --- /dev/null +++ b/public/register/index.html @@ -0,0 +1,36 @@ + + + + + MaoTube + + + +

MaoTube

+
+ + + + +
+
+ +
+
+
+ + +
+ + + + + diff --git a/public/register/script.js b/public/register/script.js new file mode 100644 index 0000000..ca6b8a1 --- /dev/null +++ b/public/register/script.js @@ -0,0 +1,46 @@ +function error(message) { + const errorContainer = document.getElementById("mao-error"); + const errorElement = document.getElementById("mao-error-message"); + + errorElement.innerText = message; + errorContainer.classList.remove("hidden"); +} + +function displayResponse(response) { + if (!response.ok) { + error(response.error); + return; + } + window.location.href = "/login"; +} + +function click() { + const username = document.getElementById("username").value; + const password = document.getElementById("password").value; + + if (username.trim().length === 0) { + error("username cannot be empty"); + return; + } else if (password.trim().length === 0) { + error("password cannot be empty"); + return; + } + const method = "POST"; + const body = JSON.stringify({ username, password }); + const headers = new Headers({ "Content-Type": "application/json" }); + return fetch("/api/register", { + method, + body, + headers, + }) + .then(v => v.json()) + .then(displayResponse); + +} + +function main() { + const submit = document.getElementById("submit"); + submit.addEventListener("click", () => click()) +} + +main(); diff --git a/public/script.js b/public/script.js deleted file mode 100644 index b28b04f..0000000 --- a/public/script.js +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/public/search/index.html b/public/search/index.html new file mode 100644 index 0000000..a0f1844 --- /dev/null +++ b/public/search/index.html @@ -0,0 +1,33 @@ + + + + + MaoTube + + + +

MaoTube

+
+ + + +
+
+
+
+ + +
+ + + + + diff --git a/public/search/script.js b/public/search/script.js new file mode 100644 index 0000000..3051e50 --- /dev/null +++ b/public/search/script.js @@ -0,0 +1,40 @@ + +function error(message) { + const errorContainer = document.getElementById("mao-error"); + const errorElement = document.getElementById("mao-error-message"); + + errorElement.innerText = message; + errorContainer.classList.remove("hidden"); +} + +function displayResponse(response) { + if (!response.ok) { + error(response.error); + return; + } + const { videos, total } = response; + if (videos.length === 0) { + error("search returned no results"); + return; + } + const resultElement = document.getElementById("result"); + resultElement.innerHTML = `

Showing ${videos.length}/${total} results.

` + + ""; +} + +function main() { + const params = new URLSearchParams(window.location.search); + const searchElement = document.getElementById("query"); + searchElement.value = params.get("query") || ""; + return fetch(`/api/search?${params.toString()}`) + .then(v => v.json()) + .then(displayResponse); +} + +main(); diff --git a/public/style.css b/public/style.css index 4751a62..e43ef4e 100644 --- a/public/style.css +++ b/public/style.css @@ -11,4 +11,50 @@ body { margin: 0 auto; padding: 2rem; text-align: center; + font-family: system-ui, sans-serif; +} + +.mao-error { + display: flex; + flex-direction: column; + background: url("/chairman_1.jpg"); + background-size: auto 600px; + background-repeat: no-repeat; + background-position: center center; + height: 600px; +} + +.hidden { + display: none; +} + +.mao-error p { + color: #fff; + text-transform: uppercase; + font-weight: bold; + font-family: "Impact", "Bebas", "League Gothic", "Oswald", "Coluna", "Ubuntu Condensed", system-ui, sans-serif; + text-shadow: + 3px 3px 0 black, + -3px -3px 0 black, + 3px -3px 0 black, + -3px 3px 0 black, + + -3px 0px 0 black, + 3px 0px 0 black, + 0px 3px 0 black, + 0px -3px 0 black; + font-size: 2em; +} + +.mao-error p:last-child { + margin-top: auto; +} + +ul#video-list { + padding: 0; + list-style: none; +} + +#video-player { + max-height: 80vh; } diff --git a/public/upload/index.html b/public/upload/index.html new file mode 100644 index 0000000..ed868c2 --- /dev/null +++ b/public/upload/index.html @@ -0,0 +1,22 @@ + + + + + MaoTube + + + +

MaoTube

+
+ + + + +
+
+ +
+ + + + diff --git a/public/watch/index.html b/public/watch/index.html new file mode 100644 index 0000000..5ef78bb --- /dev/null +++ b/public/watch/index.html @@ -0,0 +1,32 @@ + + + + + MaoTube + + + +

MaoTube

+
+ + + +
+
+
+ + +
+ + + + + diff --git a/public/watch/script.js b/public/watch/script.js new file mode 100644 index 0000000..b9add8a --- /dev/null +++ b/public/watch/script.js @@ -0,0 +1,31 @@ + +function error(message) { + const errorContainer = document.getElementById("mao-error"); + const errorElement = document.getElementById("mao-error-message"); + + errorElement.innerText = message; + errorContainer.classList.remove("hidden"); +} + +function main() { + const params = new URLSearchParams(window.location.search); + const id = params.get("id"); + if (!id) { + error("invalid id parameter"); + return; + } + + const result = document.getElementById("result"); + + const video = document.createElement("video"); + video.controls = true; + video.id = "video-player"; + video.src = `/videos/${id}.mp4`; + result.appendChild(video); + video.onerror = () => { + video.remove(); + error("invalid id parameter"); + } +} + +main(); diff --git a/videos/.gitkeep b/videos/.gitkeep new file mode 100644 index 0000000..e69de29