305 lines
9.3 KiB
JavaScript
Executable File
305 lines
9.3 KiB
JavaScript
Executable File
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)) {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
]
|