eirtube/api/downloadApi.js

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) {
// Case 1: not in memory but exists as a file. probably downloaded on a previous server run
if (dlData == undefined && downloader.videoExists(videoName))
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
}
}
}
]