const fs = require('fs') const {render} = require("pinski/plugins") const path = require("path") const fetch = require("node-fetch") const constants = require("../utils/constants") const {getUser} = require("../utils/getuser") const db = require("../utils/db") const cacheManager = require("../eirtubeMods/cache-manager") const downloader = require("../eirtubeMods/downloader") const ratelimiting = require("../eirtubeMods/ratelimiting") function serveFile(path, range, res, req, responseHeaders) { responseHeaders.Location = path const stats = fs.statSync(path) // Download all at once if (range == "all") range = `bytes=0-${stats.size}` if (!range) return { statusCode: 416, contentType: "text/html; charset=UTF-8", content: "Wrong range", headers: responseHeaders } // Stolen DIRECTLY from Pinski internals :3 let ranged = toRange(stats.size, responseHeaders, req) responseHeaders["Content-Length"] = ranged.length res.writeHead(ranged.statusCode, Object.assign({"Content-Type": "video/mp4"}, responseHeaders)) if (req.method == "HEAD") return res.end() let stream = fs.createReadStream(path, {start: ranged.start, end: ranged.end}) stream.pipe(res) return null } // Stolen DIRECTLY from Pinski internals :3 function toRange(length, headers, req) { if (length === 0) return {statusCode: 200, start: undefined, end: undefined, length: 0} let start = 0 let end = length-1 let statusCode = 200 if (req.headers.range) { let match = req.headers.range.match(/^bytes=([0-9]*)-([0-9]*)$/) if (match) { if (match[1]) { let value = +match[1] if (!isNaN(value) && value >= start) { start = value statusCode = 206 } } if (match[2]) { let value = +match[2] if (!isNaN(value) && value <= end) { end = value statusCode = 206 } } } } if (start > end) { start = 0 end = length-1 statusCode = 200 } if (statusCode == 206) { headers["Accept-Ranges"] = "bytes" headers["Content-Range"] = "bytes "+start+"-"+end+"/"+length } return {statusCode, start, end, length: end - start + 1} } module.exports = [ { route: "/getVideo", methods: ["GET", "HEAD"], code: async ({req, url, res}) => { const responseHeaders = { Location: req.url } let videoID = url.searchParams.get("v") if (videoID) { const segs = videoID.split("/") videoID = segs[segs.length - 1] } let quality = url.searchParams.get("q") if (quality) { const segs = quality.split("/") quality = segs[segs.length - 1] } // Check file exists const fname = `${videoID}-${quality}` const dlStatus = downloader.downloadStatus(fname) // Wait to download if (dlStatus == 1) await new Promise(resolve => setTimeout(() => { if (dlStatus.status > 1) resolve() }, 1000 * 2)) // 404 else if (!downloader.videoExists(fname)) { console.log(`dlstatus for ${fname}: ${dlStatus}`) return { statusCode: 404, contentType: "text/html; charset=UTF-8", content: "Not found", headers: responseHeaders } } return serveFile(path.join(constants.server_setup.video_dl_path, `${fname}.mp4`), url.searchParams.get("dl") == 1 ? "all" : req.headers.range, res, req, responseHeaders) } }, { route: "/getCaption", methods: ["GET", "HEAD"], code: async ({req, url, res}) => { const responseHeaders = { Location: req.url } // NOTE: Default NewLeaf has an oversight where it formats urls with ?lang= instead of ?label= if caption track is not auto-generated. // Why? I don't know. It means this won't work. Hope users are using my newleaf where it's fixed (or invidious). const remotePath = url.searchParams.get("url") const captionExtension = remotePath.split("/api/v1/captions/")[1] const captionFilename = captionExtension.split("?label=")[0] + "-" + captionExtension.split("?label=")[1] + ".vvt" const out = path.join(constants.server_setup.video_dl_path, captionFilename) // Needs download const needsDownload = !fs.existsSync(out) const redownloading = !needsDownload && url.searchParams.get("redownload") == 1 if (needsDownload && req.method == "HEAD") return render(404, "pug/errors/message-error.pug", { error: new Error("File not found, needs download") }, responseHeaders) const checkAuth = ratelimiting.authorize(req, needsDownload || redownloading, responseHeaders) if (checkAuth != true) return checkAuth // Check if file already exists if (fs.existsSync(out)) if (redownloading) fs.unlinkSync(out) else return serveFile(out, req.headers.range || "all", res, req, responseHeaders) // Download const instanceOrigin = getUser(req).getSettingsOrDefaults().instance const fetchURL = new URL(remotePath, instanceOrigin) // Force to match caption string if (!remotePath.toString().startsWith("/api/v1/captions/")) return { statusCode: 401, content: "EirTube: Unauthorized", contentType: "text/plain", headers: responseHeaders } let stream = fs.createWriteStream(out, { flags: "a" }) let response = await fetch(fetchURL) await response.body.pipe(stream, { end: false }) await new Promise(resolve => response.body.on("end", resolve)) stream.end() return serveFile(out, "all", res, req, responseHeaders) } }, { route: "/getOgg", methods: ["GET", "HEAD"], code: async ({req, url, res}) => { const responseHeaders = { Location: req.url } let videoID = url.searchParams.get("videoID") const pathBase = path.join(__dirname, "../") const out = path.join(constants.server_setup.ogg_dl_path, `${videoID}.ogg`) // Needs download const needsDownload = !fs.existsSync(out) if (needsDownload && req.method == "HEAD") return render(404, "pug/errors/message-error.pug", { error: new Error("File not found, needs extraction") }, responseHeaders) const checkAuth = ratelimiting.authorize(req, needsDownload, responseHeaders) if (checkAuth != true) return checkAuth // Check if file already exists if (fs.existsSync(out)) return serveFile(out, "all", res, req, responseHeaders) // Get input path let pathIn = path.join(constants.server_setup.video_dl_path, `${videoID}.mp4`) if (!fs.existsSync(pathIn)) return render(404, "pug/errors/message-error.pug", { error: new Error("Input video file missing") }, responseHeaders) // Check already downloading const dlData = cacheManager.read("ogg")[videoID] if (dlData != undefined && dlData.status == 1) await new Promise(resolve => { setTimeout(() => { if (dlData.status == 2) // It's 2 for ogg. resolve() }, 1000) }) else { let result = await downloader.createFfmpegOgg(videoID, path.join(pathBase, pathIn), path.join(pathBase, out)) if (result.error) return render(500, "pug/errors/message-error.pug", { error: new Error(result.error) }, responseHeaders) // Set timer to delete ogg after a bit else setTimeout(() => { if (fs.existsSync(out)) fs.unlinkSync(out) }, constants.server_setup.time_before_ogg_delete) } return serveFile(out, "all", res, req, responseHeaders) } }, { route: "/redownloadVideo", methods: ["GET"], code: async ({req, url}) => { const responseHeaders = { Location: req.url } const videoID = url.searchParams.get("videoID") const quality = url.searchParams.get("quality") let filename = `${videoID}-${quality}` let dlData = cacheManager.read("download")[filename] // Needs download const needsDownload = (dlData == undefined || dlData.status == 3) && downloader.videoExists(filename) const checkAuth = ratelimiting.authorize(req, needsDownload, responseHeaders) if (checkAuth != true) return checkAuth // Video is still downloading if (dlData != undefined && dlData.status < 3) { return { statusCode: 500, contentType: "application/json", content: { filename, status: "Still downloading!" }, headers: responseHeaders } // Video doesn't exist } else if (!downloader.videoExists(filename)) { return { statusCode: 500, contentType: "application/json", content: { filename, status: "Video doesn't exist." }, headers: responseHeaders } // Video exists } else { fs.unlinkSync(path.join(constants.server_setup.video_dl_path, `${filename}.mp4`)) return { statusCode: 307, contentType: "text/html; charset=UTF-8", content: "Redirecting...", headers: { ...responseHeaders, Location: `/watch?v=${videoID}${quality ? `&quality=${quality}` : ""}` } } } } }, { route: "/cacheInfo", methods: ["GET"], code: async ({req, url}) => { const responseHeaders = { Location: req.url } const checkAuth = ratelimiting.authorize(req, false, responseHeaders) if (checkAuth != true) return checkAuth let videoName = url.searchParams.get("videoName") // Includes quality let dlData = cacheManager.read("download")[videoName] let status = "not found" if (videoName && videoName != "" && downloader.videoExists(videoName)) { // Case 1: not in memory but exists as a file. probably downloaded on a previous server run if (dlData == undefined) status = "found" // Case 2: in memory else if (dlData) status = dlData.status < 3 ? "downloading" : "found" } return { statusCode: 200, contentType: "application/json", content: { videoName, status }, headers: responseHeaders } } } ]