import { createReadStream, existsSync } from "node:fs" import path from "node:path" import { validate as uuidValidate } from "uuid" import express from "express" import { Video, VideoStatus, VideoAPIRequest } from "./types.mts" import { parseVideoRequest } from "./utils/parseVideoRequest.mts" import { savePendingVideo } from "./scheduler/savePendingVideo.mts" import { getVideo } from "./scheduler/getVideo.mts" import { main } from "./main.mts" import { completedFilesDirFilePath } from "./config.mts" import { markVideoAsToDelete } from "./scheduler/markVideoAsToDelete.mts" import { markVideoAsToAbort } from "./scheduler/markVideoAsToAbort.mts" import { markVideoAsToPause } from "./scheduler/markVideoAsToPause.mts" import { markVideoAsPending } from "./scheduler/markVideoAsPending.mts" import { getPendingVideos } from "./scheduler/getPendingVideos.mts" import { hasValidAuthorization } from "./utils/hasValidAuthorization.mts" import { getAllVideosForOwner } from "./scheduler/getAllVideosForOwner.mts" import { initFolders } from "./initFolders.mts" import { sortVideosByYoungestFirst } from "./utils/sortVideosByYoungestFirst.mts" initFolders() // to disable all processing (eg. to debug) // then comment the following line: main() const app = express() const port = 7860 app.use(express.json()) app.post("/:ownerId", async (req, res) => { const request = req.body as VideoAPIRequest if (!hasValidAuthorization(req.headers)) { console.log("Invalid authorization") res.status(401) res.write(JSON.stringify({ error: "invalid token" })) res.end() return } const ownerId = req.params.ownerId if (!uuidValidate(ownerId)) { console.error("invalid owner id") res.status(400) res.write(JSON.stringify({ error: `invalid owner id` })) res.end() return } let video: Video = null console.log(`creating video from request..`) console.log(`request: `, JSON.stringify(request)) try { video = await parseVideoRequest(ownerId, request) } catch (err) { console.error(`failed to create video: ${video} (${err})`) res.status(400) res.write(JSON.stringify({ error: "query seems to be malformed" })) res.end() return } console.log(`saving video ${video.id}`) try { await savePendingVideo(video) res.status(200) res.write(JSON.stringify(video)) res.end() } catch (err) { console.error(err) res.status(500) res.write(JSON.stringify({ error: "couldn't save the video" })) res.end() } }) app.get("/:ownerId/:videoId\.mp4", async (req, res) => { /* for simplicity, let's skip auth when fetching videos the UUIDs cannot easily be guessed anyway if (!hasValidAuthorization(req.headers)) { console.log("Invalid authorization") res.status(401) res.write(JSON.stringify({ error: "invalid token" })) res.end() return } */ const ownerId = req.params.ownerId console.log("downloading..") if (!uuidValidate(ownerId)) { console.error("invalid owner id") res.status(400) res.write(JSON.stringify({ error: `invalid owner id` })) res.end() return } const videoId = req.params.videoId if (!uuidValidate(videoId)) { console.error("invalid video id") res.status(400) res.write(JSON.stringify({ error: `invalid video id` })) res.end() return } let video: Video = null try { video = await getVideo(ownerId, videoId) console.log(`returning video ${videoId} to owner ${ownerId}`) } catch (err) { res.status(404) res.write(JSON.stringify({ error: "this video doesn't exist" })) res.end() return } const completedFilePath = path.join(completedFilesDirFilePath, video.fileName) // note: we DON'T want to use the pending file path, as there may be operations on it // (ie. a process might be busy writing stuff to it) const filePath = existsSync(completedFilePath) ? completedFilePath : "" if (!filePath) { res.status(400) res.write(JSON.stringify({ error: "video exists, but cannot be previewed yet" })) res.end() return } // file path exists, let's try to read it try { // do we need this? // res.status(200) // res.setHeader("Content-Type", "media/mp4") console.log(`creating a video read stream from ${filePath}`) const stream = createReadStream(filePath) stream.on('close', () => { console.log(`finished streaming the video`) res.end() }) stream.pipe(res) } catch (err) { console.error(`failed to read the video file at ${filePath}: ${err}`) res.status(500) res.write(JSON.stringify({ error: "failed to read the video file" })) res.end() } }) // get metadata (json) app.get("/:ownerId/:videoId", async (req, res) => { if (!hasValidAuthorization(req.headers)) { console.log("Invalid authorization") res.status(401) res.write(JSON.stringify({ error: "invalid token" })) res.end() return } const ownerId = req.params.ownerId if (!uuidValidate(ownerId)) { console.error("invalid owner id") res.status(400) res.write(JSON.stringify({ error: `invalid owner id` })) res.end() return } const videoId = req.params.videoId if (!uuidValidate(videoId)) { console.error("invalid video id") res.status(400) res.write(JSON.stringify({ error: `invalid video id` })) res.end() return } try { const video = await getVideo(ownerId, videoId) res.status(200) res.write(JSON.stringify(video)) res.end() } catch (err) { console.error(err) res.status(404) res.write(JSON.stringify({ error: "couldn't find this video" })) res.end() } }) // only get the videos for a specific owner app.get("/:ownerId", async (req, res) => { if (!hasValidAuthorization(req.headers)) { console.log("Invalid authorization") res.status(401) res.write(JSON.stringify({ error: "invalid token" })) res.end() return } const ownerId = req.params.ownerId if (!uuidValidate(ownerId)) { console.error(`invalid owner d ${ownerId}`) res.status(400) res.write(JSON.stringify({ error: `invalid owner id ${ownerId}` })) res.end() return } try { const videos = await getAllVideosForOwner(ownerId) sortVideosByYoungestFirst(videos) res.status(200) res.write(JSON.stringify(videos, null, 2)) res.end() } catch (err) { console.error(err) res.status(500) res.write(JSON.stringify({ error: `couldn't get the videos for owner ${ownerId}` })) res.end() } }) // get all pending videos - this is for admin usage only app.get("/", async (req, res) => { if (!hasValidAuthorization(req.headers)) { // this is what users will see in the space - but no need to show something scary console.log("Invalid authorization") res.status(200) res.write(`
This space is the REST API used by VideoChain UI: