Compare commits
3 Commits
8852a6f0c3
...
07ba96295d
Author | SHA1 | Date | |
---|---|---|---|
07ba96295d | |||
cd124f916e | |||
a9b0c2a602 |
181
index.js
181
index.js
@ -8,8 +8,12 @@ import levenshtein from "js-levenshtein";
|
|||||||
import cookieParser from "cookie-parser";
|
import cookieParser from "cookie-parser";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import sqlite3 from "sqlite3";
|
import sqlite3 from "sqlite3";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
|
||||||
|
const PORT = 8000;
|
||||||
|
|
||||||
let sessions = [];
|
let sessions = [];
|
||||||
|
let videoQueue = [];
|
||||||
|
|
||||||
const db = new sqlite3.Database("database.sqlite3");
|
const db = new sqlite3.Database("database.sqlite3");
|
||||||
|
|
||||||
@ -25,6 +29,22 @@ function dbRun(query, ...parameters) {
|
|||||||
return new Promise((resolve, reject) => db.run(query, parameters, (err, data) => err ? reject(err) : resolve(data)));
|
return new Promise((resolve, reject) => db.run(query, parameters, (err, data) => err ? reject(err) : resolve(data)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runCommand(cmd, args) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const process = childProcess.spawn(cmd, args);
|
||||||
|
|
||||||
|
let data = "";
|
||||||
|
let error = false;
|
||||||
|
process.stdout.on("data", chunk => data += chunk);
|
||||||
|
process.stderr.on("data", chunk => {
|
||||||
|
error = true;
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("close", () => resolve({ error, data }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function videoPath(id) {
|
function videoPath(id) {
|
||||||
return `videos/${id}.webm`;
|
return `videos/${id}.webm`;
|
||||||
}
|
}
|
||||||
@ -42,6 +62,35 @@ function dirname() {
|
|||||||
return path.dirname(fileURLToPath(import.meta.url));
|
return path.dirname(fileURLToPath(import.meta.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function authorized() {
|
||||||
|
return async (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 = await dbGet("SELECT * FROM users WHERE id = ?", session.userId);
|
||||||
|
if (user === undefined) {
|
||||||
|
throw new Error("error: session with invalid userId");
|
||||||
|
}
|
||||||
|
req.user = user;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@ -125,35 +174,6 @@ app.get("/api/search", async (req, res) => {
|
|||||||
return res.status(200).json({ ok: true, videos, total });
|
return res.status(200).json({ ok: true, videos, total });
|
||||||
});
|
});
|
||||||
|
|
||||||
function authorized() {
|
|
||||||
return async (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 = await dbGet("SELECT * FROM users WHERE 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) => {
|
app.get("/api/logout", authorized(), (req, res) => {
|
||||||
sessions = sessions.filter(session => session.userId !== req.user.id);
|
sessions = sessions.filter(session => session.userId !== req.user.id);
|
||||||
return res.status(200).json({ ok: true });
|
return res.status(200).json({ ok: true });
|
||||||
@ -167,7 +187,7 @@ app.post("/api/upload-video", authorized(), fileUpload({ limits: { fileSize: 2 *
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (req.files.video.mimetype !== "video/mp4") {
|
if (req.files.video.mimetype !== "video/mp4") {
|
||||||
return res.status(400).json({ ok: false, error: "bad mimetype" });
|
return res.status(415).json({ ok: false, error: "bad mimetype" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
@ -176,33 +196,52 @@ app.post("/api/upload-video", authorized(), fileUpload({ limits: { fileSize: 2 *
|
|||||||
const newPath = path.join(dirname(), videoPath(id));
|
const newPath = path.join(dirname(), videoPath(id));
|
||||||
const thumbnailPath = path.join(dirname(), "videos", `${id}.png`);
|
const thumbnailPath = path.join(dirname(), "videos", `${id}.png`);
|
||||||
|
|
||||||
console.log(newPath);
|
const durationResult = await runCommand("ffprobe", ["-i", tempPath, "-show_format", "-loglevel", "error"]);
|
||||||
|
if (durationResult.error) {
|
||||||
let exitCode = await new Promise(resolve => {
|
return res.status(400).json({ ok: false, error: "invalid video file" });
|
||||||
const process = childProcess.spawn("ffmpeg", ["-i", tempPath, "-b:v", "1M", "-b:a", "192k", newPath]);
|
|
||||||
process.stderr.on("data", (data) => console.error(data.toString()));
|
|
||||||
process.on("close", resolve);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
throw new Error("ffmpeg failed");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exitCode = await new Promise(resolve => {
|
const duration = parseFloat(durationResult.data.match(/duration=([.\d]+)/)[1]) * 1_000_000;;
|
||||||
const process = childProcess.spawn("ffmpeg", ["-i", tempPath, "-ss", "00:00:01.000", "-vframes", "1", thumbnailPath]);
|
|
||||||
process.stderr.on("data", (data) => console.error(data.toString()));
|
const queueItem = {
|
||||||
process.on("close", resolve);
|
videoId: id,
|
||||||
|
userId,
|
||||||
|
errors: [],
|
||||||
|
progress: 0,
|
||||||
|
duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
videoQueue.push(queueItem);
|
||||||
|
|
||||||
|
const progressPath = path.join(dirname(), "tmp", "progress", `${id}.txt`)
|
||||||
|
const uploadProcess = childProcess.spawn("ffmpeg", ["-i", tempPath, "-b:v", "1M", "-b:a", "192k", "-progress", progressPath, "-loglevel", "error", newPath]);
|
||||||
|
const thumbnailProcess = childProcess.spawn("ffmpeg", ["-i", tempPath, "-ss", "00:00:01.000", "-vframes", "1", "-loglevel", "error", thumbnailPath]);
|
||||||
|
|
||||||
|
uploadProcess.stderr.on("data", data => queueItem.errors.push(data.toString()));
|
||||||
|
thumbnailProcess.stderr.on("data", data => queueItem.errors.push(data.toString()));
|
||||||
|
|
||||||
|
uploadProcess.on("close", () => {
|
||||||
|
if (queueItem.errors.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbRun("INSERT INTO videos (id, user_id, title) VALUES (?, ?, ?)", id, userId, title);
|
||||||
|
|
||||||
|
const index = videoQueue.indexOf(item => item.videoId === queueItem.videoId)
|
||||||
|
videoQueue.splice(index, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
throw new Error("thumbnail generation failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
await dbRun("INSERT INTO videos (id, user_id, title) VALUES (?, ?, ?)", id, userId, title);
|
|
||||||
|
|
||||||
return res.status(200).json({ ok: true });
|
return res.status(200).json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get("/api/video-queue", authorized(), (req, res) => {
|
||||||
|
const userId = req.user.id;
|
||||||
|
|
||||||
|
const queue = videoQueue.filter(item => item.userId === userId);
|
||||||
|
|
||||||
|
return res.status(200).json({ ok: true, queue });
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/api/video-info", async (req, res) => {
|
app.get("/api/video-info", async (req, res) => {
|
||||||
const id = req.query["id"];
|
const id = req.query["id"];
|
||||||
|
|
||||||
@ -228,6 +267,42 @@ app.use((err, req, res, next) => {
|
|||||||
res.status(500).json({ ok: false, error: "server error" })
|
res.status(500).json({ ok: false, error: "server error" })
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(8000, () => {
|
async function main() {
|
||||||
console.log("app at http://localhost:8000/");
|
(async () => {
|
||||||
})
|
const progressPath = path.join(dirname(), "tmp", "progress");
|
||||||
|
|
||||||
|
await fs.mkdir(progressPath, { recursive: true });
|
||||||
|
|
||||||
|
const watch = fs.watch(progressPath, { recursive: true });
|
||||||
|
|
||||||
|
for await (const event of watch) {
|
||||||
|
const videoId = event.filename.replace(".txt", "");
|
||||||
|
const queueItem = videoQueue.find(item => item.videoId === videoId);
|
||||||
|
|
||||||
|
if (!queueItem) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.readFile(path.join(progressPath, event.filename), "utf-8")
|
||||||
|
.then(string => {
|
||||||
|
const index = string.lastIndexOf("out_time_ms=");
|
||||||
|
if (index === -1) return;
|
||||||
|
|
||||||
|
const length = parseInt(string.slice(index).match(/out_time_ms=(\d+)/)[1]);
|
||||||
|
|
||||||
|
for (const i in videoQueue) {
|
||||||
|
if (videoQueue[i].videoId !== videoId) continue;
|
||||||
|
|
||||||
|
videoQueue[i].progress = length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log("app at http://localhost:8000/");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
||||||
|
8
public/helpers.js
Normal file
8
public/helpers.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
function error(message) {
|
||||||
|
const errorContainer = document.getElementById("mao-error");
|
||||||
|
const errorElement = document.getElementById("mao-error-message");
|
||||||
|
|
||||||
|
errorElement.innerText = message || "unknown error";
|
||||||
|
errorContainer.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
31
public/queue/index.html
Normal file
31
public/queue/index.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>MaoTube</title>
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
<script defer src="/helpers.js"></script>
|
||||||
|
<script defer src="script.js"></script>
|
||||||
|
<script defer src="/header.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>MaoTube</h1>
|
||||||
|
<div>
|
||||||
|
<h1>Uploading videos</h1>
|
||||||
|
</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>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
57
public/queue/script.js
Normal file
57
public/queue/script.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
function readableMicroseconds(us) {
|
||||||
|
const min = parseInt(us / 60_000_000);
|
||||||
|
const sec = parseInt((us % 60_000_000) / 1_000_000);
|
||||||
|
|
||||||
|
if (!min) {
|
||||||
|
return `${sec}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${min}m ${sec}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const response = await fetch(`/api/video-queue`);
|
||||||
|
if (!response.ok) {
|
||||||
|
error("something went wrong");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
if (!json.ok) {
|
||||||
|
error(json.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queue = json.queue;
|
||||||
|
|
||||||
|
document.getElementById("result").innerHTML =
|
||||||
|
`<p>Currently processing ${queue.length} video(s)</p>`
|
||||||
|
+ `<ul id="video-list">`
|
||||||
|
+ queue
|
||||||
|
.map(vid => {
|
||||||
|
const percentage = parseInt(vid.progress / vid.duration * 100);
|
||||||
|
const uploadedTime = readableMicroseconds(vid.progress);
|
||||||
|
const totalTime = readableMicroseconds(vid.duration);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<li>
|
||||||
|
<div class="video-item">
|
||||||
|
<div class="video-image">
|
||||||
|
<img class="shadow" src="/videos/${vid.id}.png" alt="">
|
||||||
|
<img class="non-shadow" src="/videos/${vid.id}.png" alt="">
|
||||||
|
</div>
|
||||||
|
<span class="video-info">
|
||||||
|
${vid.title}
|
||||||
|
<br>
|
||||||
|
<p>Uploaded ${uploadedTime} of ${totalTime} (${percentage}%)</p>
|
||||||
|
<progress max="${vid.duration}" value="${vid.progress}">${percentage}%</progress>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("")
|
||||||
|
+ "</ul>";
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
Loading…
Reference in New Issue
Block a user