forked from sfja.skp/maotube
176 lines
5.9 KiB
JavaScript
176 lines
5.9 KiB
JavaScript
import express from "express";
|
|
import cors from "cors";
|
|
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 = [];
|
|
const videos = [];
|
|
|
|
function randomString(length) {
|
|
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789";
|
|
let result = "";
|
|
for (let i = 0; i < length; ++i) {
|
|
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/"));
|
|
app.use("/videos", express.static("videos/"));
|
|
|
|
app.post("/api/register", async (req, res) => {
|
|
const { username, password } = req.body;
|
|
if (typeof username !== "string" || typeof password !== "string") {
|
|
return res.status(400).json({ ok: false, error: "bad request" });
|
|
}
|
|
const existingUser = users.find(user => user.username === username);
|
|
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, password: passwordHash };
|
|
users.push(user);
|
|
return res.status(200).json({ ok: true, user });
|
|
});
|
|
|
|
app.post("/api/login", async (req, res) => {
|
|
const { username, password } = req.body;
|
|
if (typeof username !== "string" || typeof password !== "string") {
|
|
return res.status(400).json({ ok: false, error: "bad request" });
|
|
}
|
|
const user = users.find(user => user.username === username);
|
|
if (user === undefined) {
|
|
return res.status(400).json({ ok: false, error: "wrong username/password" });
|
|
}
|
|
if (!await bcrypt.compare(password, user.password)) {
|
|
return res.status(400).json({ ok: false, error: "wrong username/password" });
|
|
}
|
|
sessions = sessions.filter(session => session.userId !== user.id);
|
|
const token = randomString(64);
|
|
const session = { userId: user.id, token };
|
|
sessions.push(session);
|
|
res.clearCookie("token");
|
|
res.cookie("token", token);
|
|
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 (req.cookies && "token" in req.cookies) {
|
|
return req.cookies["token"];
|
|
} else if ("token" in req.query) {
|
|
return req.query["token"];
|
|
} else if (req.method === "post" && "token" in req.body) {
|
|
return req.body["token"];
|
|
} else {
|
|
return null;
|
|
}
|
|
})();
|
|
if (token === null) {
|
|
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: "unauthorized" });
|
|
}
|
|
const user = users.find(user => user.id === session.userId);
|
|
if (user === undefined) {
|
|
throw new Error("error: session with invalid userId");
|
|
}
|
|
req.user = user;
|
|
next();
|
|
}
|
|
}
|
|
|
|
app.get("/api/logout", authorized(), (req, res) => {
|
|
sessions = sessions.filter(session => session.userId !== req.user.id);
|
|
return res.status(200).json({ ok: true });
|
|
});
|
|
|
|
app.post("/api/upload_video", authorized(), fileUpload({ limits: { fileSize: 2 ** 26 }, useTempFiles: true }), async (req, res) => {
|
|
const { title } = req.body;
|
|
if (!req.files || !req.files.video) {
|
|
return res.status(400).json({ ok: false, error: "bad request" });
|
|
}
|
|
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.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 50 MB 10 Minutes 480p30"]);
|
|
process.stderr.on("data", (data) => {
|
|
console.error(data.toString());
|
|
});
|
|
process.on("close", (code) => {
|
|
resolve(code);
|
|
})
|
|
})
|
|
if (exitCode !== 0) {
|
|
throw new Error("handbrake failed");
|
|
}
|
|
|
|
const video = {
|
|
id,
|
|
userId,
|
|
title,
|
|
path: newPath,
|
|
};
|
|
videos.push(video);
|
|
return res.status(200).json({ ok: true, video });
|
|
})
|
|
|
|
app.use((err, req, res, next) => {
|
|
console.error(err);
|
|
res.status(500).json({ ok: false, error: "server error" })
|
|
});
|
|
|
|
app.listen(8000, () => {
|
|
console.log("app at http://localhost:8000/");
|
|
})
|