maotube/index.js

234 lines
7.2 KiB
JavaScript
Raw Normal View History

2024-01-18 23:45:23 +00:00
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";
2024-01-19 02:10:56 +00:00
import levenshtein from "js-levenshtein";
2024-01-19 02:44:55 +00:00
import cookieParser from "cookie-parser";
2024-02-10 22:30:03 +00:00
import { fileURLToPath } from "url";
import sqlite3 from "sqlite3";
2024-01-18 23:45:23 +00:00
let sessions = [];
2024-02-10 22:30:03 +00:00
const db = new sqlite3.Database("database.sqlite3");
function dbGet(query, ...parameters) {
return new Promise((resolve, reject) => db.get(query, parameters, (err, data) => err ? reject(err) : resolve(data)));
2024-02-10 22:30:03 +00:00
}
function dbAll(query, ...parameters) {
return new Promise((resolve, reject) => db.all(query, parameters, (err, data) => err ? reject(err) : resolve(data)));
2024-02-10 22:30:03 +00:00
}
function dbRun(query, ...parameters) {
return new Promise((resolve, reject) => db.run(query, parameters, (err, data) => err ? reject(err) : resolve(data)));
}
function videoPath(id) {
return `videos/${id}.webm`;
2024-02-10 22:30:03 +00:00
}
2024-01-18 23:45:23 +00:00
function randomString(length) {
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789";
let result = "";
for (let i = 0; i < length; ++i) {
2024-01-19 02:44:55 +00:00
result += chars[Math.floor(chars.length * Math.random())];
2024-01-18 23:45:23 +00:00
}
return result;
}
2024-01-19 02:44:55 +00:00
function dirname() {
return path.dirname(fileURLToPath(import.meta.url));
}
2024-01-18 23:45:23 +00:00
const app = express();
app.use(cors());
app.use(express.json());
2024-01-19 02:44:55 +00:00
app.use(cookieParser());
2024-01-18 23:45:23 +00:00
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;
2024-02-10 22:30:03 +00:00
2024-01-18 23:45:23 +00:00
if (typeof username !== "string" || typeof password !== "string") {
return res.status(400).json({ ok: false, error: "bad request" });
}
2024-02-10 22:30:03 +00:00
const existingUser = await dbGet("SELECT * FROM users WHERE username = ?", username);
2024-02-10 22:30:03 +00:00
2024-01-19 02:10:56 +00:00
if (existingUser !== undefined) {
2024-01-18 23:45:23 +00:00
return res.status(400).json({ ok: false, error: "username taken" });
}
2024-02-10 22:30:03 +00:00
2024-01-18 23:45:23 +00:00
const passwordHash = await bcrypt.hash(password, 10);
2024-02-10 22:30:03 +00:00
await dbGet("INSERT INTO users (username, password) VALUES (?, ?)", username, passwordHash);
2024-02-10 22:30:03 +00:00
return res.status(200).json({ ok: true });
2024-01-18 23:45:23 +00:00
});
app.post("/api/login", async (req, res) => {
const { username, password } = req.body;
2024-02-10 22:30:03 +00:00
2024-01-18 23:45:23 +00:00
if (typeof username !== "string" || typeof password !== "string") {
return res.status(400).json({ ok: false, error: "bad request" });
}
2024-02-10 22:30:03 +00:00
const user = await dbGet("SELECT * FROM users WHERE username = ?", username);
2024-01-18 23:45:23 +00:00
if (user === undefined) {
return res.status(400).json({ ok: false, error: "wrong username/password" });
}
2024-02-10 22:30:03 +00:00
if (!await bcrypt.compare(password, user.password)) {
2024-01-18 23:45:23 +00:00
return res.status(400).json({ ok: false, error: "wrong username/password" });
}
2024-02-10 22:30:03 +00:00
2024-01-18 23:45:23 +00:00
sessions = sessions.filter(session => session.userId !== user.id);
2024-02-10 22:30:03 +00:00
2024-01-18 23:45:23 +00:00
const token = randomString(64);
const session = { userId: user.id, token };
2024-02-10 22:30:03 +00:00
2024-01-18 23:45:23 +00:00
sessions.push(session);
2024-02-10 22:30:03 +00:00
2024-01-18 23:45:23 +00:00
res.clearCookie("token");
res.cookie("token", token);
2024-02-10 22:30:03 +00:00
2024-01-18 23:45:23 +00:00
return res.status(200).json({ ok: true, session });
});
2024-01-19 02:10:56 +00:00
app.get("/api/search", async (req, res) => {
const page = +req.query.page || 0;
const search = req.query.query;
2024-02-10 22:30:03 +00:00
2024-01-19 02:10:56 +00:00
if (!search) {
return res.status(400).json({ ok: false, error: "bad request" });
}
2024-02-10 22:30:03 +00:00
const [start, end] = [20 * page, 20];
const videos = await dbAll(`
SELECT videos.*, users.username AS author
FROM videos
JOIN users ON users.id = videos.user_id
WHERE title LIKE CONCAT('%', ?, '%')
LIMIT ?
OFFSET ?
`, search, end, start);
2024-02-10 22:30:03 +00:00
const { total } = await dbGet("SELECT COUNT(*) AS total FROM videos");
2024-02-10 22:30:03 +00:00
return res.status(200).json({ ok: true, videos, total });
2024-01-19 02:10:56 +00:00
});
2024-01-18 23:45:23 +00:00
function authorized() {
2024-02-10 22:30:03 +00:00
return async (req, res, next) => {
2024-01-18 23:45:23 +00:00
const token = (() => {
2024-01-19 02:44:55 +00:00
if (req.cookies && "token" in req.cookies) {
2024-01-18 23:45:23 +00:00
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) {
2024-01-19 02:44:55 +00:00
return res.status(400).json({ ok: false, error: "unauthorized" });
2024-01-18 23:45:23 +00:00
}
const session = sessions.find(session => session.token === token);
if (session === undefined) {
2024-01-19 02:44:55 +00:00
return res.status(400).json({ ok: false, error: "unauthorized" });
2024-01-18 23:45:23 +00:00
}
2024-02-10 22:30:03 +00:00
const user = await dbGet("SELECT * FROM users WHERE id = ?", session.userId);
2024-01-18 23:45:23 +00:00
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) => {
2024-01-18 23:45:23 +00:00
const { title } = req.body;
2024-02-10 22:30:03 +00:00
2024-01-19 02:44:55 +00:00
if (!req.files || !req.files.video) {
2024-01-18 23:45:23 +00:00
return res.status(400).json({ ok: false, error: "bad request" });
}
2024-02-10 22:30:03 +00:00
2024-01-19 02:44:55 +00:00
if (req.files.video.mimetype !== "video/mp4") {
2024-01-18 23:45:23 +00:00
return res.status(400).json({ ok: false, error: "bad mimetype" });
}
2024-02-10 22:30:03 +00:00
2024-01-18 23:45:23 +00:00
const userId = req.user.id;
const id = randomString(4);
2024-01-19 02:44:55 +00:00
const tempPath = req.files.video.tempFilePath;
const newPath = path.join(dirname(), videoPath(id));
2024-02-10 23:02:33 +00:00
const thumbnailPath = path.join(dirname(), "videos", `${id}.png`);
2024-02-10 22:30:03 +00:00
2024-01-19 02:44:55 +00:00
console.log(newPath);
2024-01-18 23:45:23 +00:00
2024-02-10 23:02:33 +00:00
let exitCode = await new Promise(resolve => {
2024-02-10 22:30:03 +00:00
const process = childProcess.spawn("ffmpeg", ["-i", tempPath, "-b:v", "1M", "-b:a", "192k", newPath]);
2024-02-10 23:02:33 +00:00
process.stderr.on("data", (data) => console.error(data.toString()));
process.on("close", resolve);
2024-02-10 22:30:03 +00:00
});
2024-01-18 23:45:23 +00:00
if (exitCode !== 0) {
2024-02-10 22:30:03 +00:00
throw new Error("ffmpeg failed");
2024-01-18 23:45:23 +00:00
}
2024-02-10 23:02:33 +00:00
exitCode = await new Promise(resolve => {
const process = childProcess.spawn("ffmpeg", ["-i", tempPath, "-ss", "00:00:01.000", "-vframes", "1", thumbnailPath]);
process.stderr.on("data", (data) => console.error(data.toString()));
process.on("close", resolve);
});
if (exitCode !== 0) {
throw new Error("thumbnail generation failed");
}
await dbRun("INSERT INTO videos (id, user_id, title) VALUES (?, ?, ?)", id, userId, title);
2024-02-10 22:30:03 +00:00
return res.status(200).json({ ok: true });
});
app.get("/api/video-info", async (req, res) => {
const id = req.query["id"];
const video = await dbGet(`
SELECT videos.id, videos.title, users.username AS author
FROM videos
JOIN users ON users.id = videos.user_id
WHERE videos.id = ?
LIMIT 1
`, id);
if (!video) {
return res.status(404).json({ ok: false, error: "video not found" });
}
video.path = path.join("/", videoPath(id));
return res.status(200).json({ ok: true, video });
});
2024-01-18 23:45:23 +00:00
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/");
})