forked from sfja.skp/maotube
Merge pull request 'usable application' (#1) from pieter/maotube:main into main
Reviewed-on: sfja.skp/maotube#1
This commit is contained in:
commit
e7660fda7c
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
videos/*
|
videos/*
|
||||||
|
!videos/.gitkeep
|
||||||
|
tmp/
|
||||||
|
60
index.js
60
index.js
@ -4,6 +4,9 @@ import bcrypt from "bcrypt";
|
|||||||
import fileUpload from "express-fileupload";
|
import fileUpload from "express-fileupload";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import childProcess from "child_process";
|
import childProcess from "child_process";
|
||||||
|
import levenshtein from "js-levenshtein";
|
||||||
|
import cookieParser from "cookie-parser";
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
const users = [];
|
const users = [];
|
||||||
let sessions = [];
|
let sessions = [];
|
||||||
@ -13,14 +16,19 @@ function randomString(length) {
|
|||||||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789";
|
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789";
|
||||||
let result = "";
|
let result = "";
|
||||||
for (let i = 0; i < length; ++i) {
|
for (let i = 0; i < length; ++i) {
|
||||||
result += chars[chars.length * Math.random()];
|
result += chars[Math.floor(chars.length * Math.random())];
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dirname() {
|
||||||
|
return path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
}
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
app.use(cookieParser());
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
app.use("/", express.static("public/"));
|
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" });
|
return res.status(400).json({ ok: false, error: "bad request" });
|
||||||
}
|
}
|
||||||
const existingUser = users.find(user => user.username === username);
|
const existingUser = users.find(user => user.username === username);
|
||||||
if (existingUser === undefined) {
|
if (existingUser !== undefined) {
|
||||||
return res.status(400).json({ ok: false, error: "username taken" });
|
return res.status(400).json({ ok: false, error: "username taken" });
|
||||||
}
|
}
|
||||||
const passwordHash = await bcrypt.hash(password, 10);
|
const passwordHash = await bcrypt.hash(password, 10);
|
||||||
const id = users.length;
|
const id = users.length;
|
||||||
const user = { id, username, passwordHash };
|
const user = { id, username, password: passwordHash };
|
||||||
users.push(user);
|
users.push(user);
|
||||||
return res.status(200).json({ ok: true, 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 });
|
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() {
|
function authorized() {
|
||||||
return (req, res, next) => {
|
return (req, res, next) => {
|
||||||
const token = (() => {
|
const token = (() => {
|
||||||
if ("token" in req.cookies) {
|
if (req.cookies && "token" in req.cookies) {
|
||||||
return req.cookies["token"];
|
return req.cookies["token"];
|
||||||
} else if ("token" in req.query) {
|
} else if ("token" in req.query) {
|
||||||
return req.query["token"];
|
return req.query["token"];
|
||||||
@ -77,13 +108,13 @@ function authorized() {
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
if (token === null) {
|
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);
|
const session = sessions.find(session => session.token === token);
|
||||||
if (session === undefined) {
|
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) {
|
if (user === undefined) {
|
||||||
throw new Error("error: session with invalid userId");
|
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) => {
|
app.post("/api/upload_video", authorized(), fileUpload({ limits: { fileSize: 2 ** 26 }, useTempFiles: true }), async (req, res) => {
|
||||||
const { title } = req.body;
|
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" });
|
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" });
|
return res.status(400).json({ ok: false, error: "bad mimetype" });
|
||||||
}
|
}
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
const id = randomString(4);
|
const id = randomString(4);
|
||||||
const tempPath = req.files[0].tempFilePath;
|
const tempPath = req.files.video.tempFilePath;
|
||||||
const newPath = path.join("/videos", id, ".mp4");
|
const newPath = path.join(dirname(), "videos", `${id}.mp4`);
|
||||||
|
console.log(newPath);
|
||||||
|
|
||||||
const exitCode = await new Promise((resolve, _reject) => {
|
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) => {
|
process.stderr.on("data", (data) => {
|
||||||
conole.error(data);
|
console.error(data.toString());
|
||||||
});
|
});
|
||||||
process.on("close", (code) => {
|
process.on("close", (code) => {
|
||||||
resolve(code);
|
resolve(code);
|
||||||
})
|
})
|
||||||
});
|
})
|
||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
throw new Error("handbrake failed");
|
throw new Error("handbrake failed");
|
||||||
}
|
}
|
||||||
|
32
package-lock.json
generated
32
package-lock.json
generated
@ -10,9 +10,11 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.18.2",
|
"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": {
|
"node_modules/@mapbox/node-pre-gyp": {
|
||||||
@ -248,6 +250,26 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/cookie-signature": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||||
@ -668,6 +690,14 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/lru-cache": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
|
@ -12,8 +12,10 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-fileupload": "^1.4.3"
|
"express-fileupload": "^1.4.3",
|
||||||
|
"js-levenshtein": "^1.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
37
public/header.js
Normal file
37
public/header.js
Normal file
@ -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 `<a href="${href}"><b>${name}</b></a>`
|
||||||
|
} else {
|
||||||
|
return `<a href="${href}">${name}</a>`
|
||||||
|
}
|
||||||
|
}).join(" - ");
|
||||||
|
|
||||||
|
document.querySelector("h1").outerHTML = `
|
||||||
|
<header>
|
||||||
|
<h1>MaoTube</h1>
|
||||||
|
<nav>
|
||||||
|
${links}
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
displayHeader();
|
@ -4,18 +4,19 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>MaoTube</title>
|
<title>MaoTube</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
<script src="script.js" defer></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>MaoTube</h1>
|
<h1>MaoTube</h1>
|
||||||
<form method="GET" target="/search">
|
<form method="GET" target="_self" action="/search">
|
||||||
<input type="text" name="query" placeholder="...">
|
<label for="query"><p>Search</p></label>
|
||||||
|
<input type="text" id="query" name="query" placeholder="...">
|
||||||
<input type="submit" value="Search">
|
<input type="submit" value="Search">
|
||||||
</form>
|
</form>
|
||||||
<br>
|
<br>
|
||||||
<img src="chairman_1.jpg" alt="The chairman" height="600">
|
<img src="chairman_1.jpg" alt="The chairman" height="600">
|
||||||
<img src="chairman_2.jpg" alt="The chairman" height="600">
|
<img src="chairman_2.jpg" alt="The chairman" height="600">
|
||||||
<img src="chairman_3.jpg" alt="The chairman" height="600">
|
<img src="chairman_3.jpg" alt="The chairman" height="600">
|
||||||
|
<script src="/header.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
36
public/login/index.html
Normal file
36
public/login/index.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>MaoTube</title>
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>MaoTube</h1>
|
||||||
|
<div>
|
||||||
|
<label for="username"><p>Username</p></label>
|
||||||
|
<input type="text" name="username" id="username">
|
||||||
|
<label for="password"><p>Password</p></label>
|
||||||
|
<input type="text" name="password" id="password">
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<input type="submit" id="submit" value="Login">
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div id="result">
|
||||||
|
<noscript>
|
||||||
|
<div class="mao-error">
|
||||||
|
<p>javascript not enabled</p>
|
||||||
|
<p>bottom text</p>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
<div id="mao-error" class="mao-error hidden">
|
||||||
|
<p id="mao-error-message"></p>
|
||||||
|
<p>bottom text</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
<script src="/header.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
46
public/login/script.js
Normal file
46
public/login/script.js
Normal file
@ -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();
|
36
public/register/index.html
Normal file
36
public/register/index.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>MaoTube</title>
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>MaoTube</h1>
|
||||||
|
<div>
|
||||||
|
<label for="username"><p>Username</p></label>
|
||||||
|
<input type="text" name="username" id="username">
|
||||||
|
<label for="password"><p>Password</p></label>
|
||||||
|
<input type="text" name="password" id="password">
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<input type="submit" id="submit" value="Register">
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div id="result">
|
||||||
|
<noscript>
|
||||||
|
<div class="mao-error">
|
||||||
|
<p>javascript not enabled</p>
|
||||||
|
<p>bottom text</p>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
<div id="mao-error" class="mao-error hidden">
|
||||||
|
<p id="mao-error-message"></p>
|
||||||
|
<p>bottom text</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
<script src="/header.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
46
public/register/script.js
Normal file
46
public/register/script.js
Normal file
@ -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();
|
@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
33
public/search/index.html
Normal file
33
public/search/index.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>MaoTube</title>
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>MaoTube</h1>
|
||||||
|
<form method="GET" target="_self" action="/search">
|
||||||
|
<label for="query"><p>Search</p></label>
|
||||||
|
<input type="text" id="query" name="query" placeholder="...">
|
||||||
|
<input type="submit" value="Search">
|
||||||
|
</form>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<div id="result">
|
||||||
|
<noscript>
|
||||||
|
<div class="mao-error">
|
||||||
|
<p>javascript not enabled</p>
|
||||||
|
<p>bottom text</p>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
<div id="mao-error" class="mao-error hidden">
|
||||||
|
<p id="mao-error-message"></p>
|
||||||
|
<p>bottom text</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
<script src="/header.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
40
public/search/script.js
Normal file
40
public/search/script.js
Normal file
@ -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 = `<p>Showing ${videos.length}/${total} results.</p>`
|
||||||
|
+ "<ul id='video-list'>"
|
||||||
|
+ videos
|
||||||
|
.map(v => {
|
||||||
|
return `<li><p><a href="/watch?id=${v.id}">${v.title}</a> - ${v.author}</p></li>`
|
||||||
|
})
|
||||||
|
.join("")
|
||||||
|
+ "</ul>";
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
@ -11,4 +11,50 @@ body {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
text-align: center;
|
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;
|
||||||
}
|
}
|
||||||
|
22
public/upload/index.html
Normal file
22
public/upload/index.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>MaoTube</title>
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>MaoTube</h1>
|
||||||
|
<form action="/api/upload_video" method="POST" enctype="multipart/form-data">
|
||||||
|
<label for="username"><p>Title</p></label>
|
||||||
|
<input type="text" name="title">
|
||||||
|
<label for="password"><p>Video</p></label>
|
||||||
|
<input type="file" name="video">
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<input type="submit" id="submit" value="Upload">
|
||||||
|
</form>
|
||||||
|
<script src="/header.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
32
public/watch/index.html
Normal file
32
public/watch/index.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>MaoTube</title>
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>MaoTube</h1>
|
||||||
|
<form method="GET" target="_self" action="/search">
|
||||||
|
<label for="query"><p>Search</p></label>
|
||||||
|
<input type="text" id="query" name="query" placeholder="...">
|
||||||
|
<input type="submit" value="Search">
|
||||||
|
</form>
|
||||||
|
<br>
|
||||||
|
<div id="result">
|
||||||
|
<noscript>
|
||||||
|
<div class="mao-error">
|
||||||
|
<p>javascript not enabled</p>
|
||||||
|
<p>bottom text</p>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
<div id="mao-error" class="mao-error hidden">
|
||||||
|
<p id="mao-error-message"></p>
|
||||||
|
<p>bottom text</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
<script src="/header.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
31
public/watch/script.js
Normal file
31
public/watch/script.js
Normal file
@ -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();
|
0
videos/.gitkeep
Normal file
0
videos/.gitkeep
Normal file
Loading…
Reference in New Issue
Block a user