2023-10-26 12:35:34 -05:00
|
|
|
const express = require("express"),
|
|
|
|
path = require("path"),
|
|
|
|
fs = require("fs"),
|
|
|
|
ytdl = require("ytdl-core"),
|
2023-10-27 00:38:23 -05:00
|
|
|
bodyParser = require("body-parser"),
|
2023-11-01 08:33:22 -05:00
|
|
|
youtube = require("scrape-youtube"),
|
2023-11-19 22:40:21 -06:00
|
|
|
ytExt = require("youtube-ext"),
|
|
|
|
cp = require("child_process"),
|
|
|
|
ffmpeg = require('ffmpeg-static')
|
2023-10-26 12:35:34 -05:00
|
|
|
|
|
|
|
const PORT = process.env.PORT || 8080
|
|
|
|
|
|
|
|
const staticPath = path.join(__dirname, 'static')
|
2023-10-27 00:38:23 -05:00
|
|
|
|
|
|
|
const cssPath = path.join(staticPath, 'mainStyle.css')
|
|
|
|
|
2023-10-26 12:35:34 -05:00
|
|
|
const resources = path.join(__dirname, 'resources')
|
|
|
|
|
2023-10-30 08:29:44 -05:00
|
|
|
const cachePath = path.join(__dirname, 'cache')
|
2023-10-30 00:57:48 -05:00
|
|
|
const searchCacheDur = (process.env.SEARCH_DUR || 24) * 3600000
|
2023-10-27 00:38:23 -05:00
|
|
|
|
2023-10-26 12:35:34 -05:00
|
|
|
const playerPath = path.join(resources, 'player.html')
|
2023-10-28 19:08:28 -05:00
|
|
|
const searchPath = path.join(resources, 'searchPage.html')
|
2023-11-01 08:33:22 -05:00
|
|
|
const channelPath = path.join(resources, 'channelPage.html')
|
2023-10-26 12:35:34 -05:00
|
|
|
|
2023-11-20 10:55:41 -06:00
|
|
|
const cssHeader = `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"
|
|
|
|
integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
|
|
|
|
<link rel="stylesheet" href="/mainStyle.css">`
|
|
|
|
|
|
|
|
const topBar = `
|
|
|
|
<div id="titleBar" class="row container-fluid">
|
|
|
|
<div class="col-6">
|
|
|
|
<h1><a href="/">SimpleTube</a></h1>
|
|
|
|
</div>
|
|
|
|
<div class="col-6">
|
|
|
|
<form action="/search">
|
|
|
|
<input type="text" placeholder="Search" name="q">
|
|
|
|
</form>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
`
|
2023-10-27 00:38:23 -05:00
|
|
|
|
|
|
|
|
|
|
|
if (fs.existsSync(cachePath)) {
|
|
|
|
fs.rmSync(cachePath, { recursive: true, force: true })
|
|
|
|
}
|
|
|
|
|
|
|
|
fs.mkdirSync(cachePath)
|
|
|
|
|
2023-10-29 23:28:17 -05:00
|
|
|
|
2023-10-27 00:38:23 -05:00
|
|
|
var videoCache = {}
|
2023-10-30 00:57:48 -05:00
|
|
|
var searchCache = {}
|
2023-10-27 00:38:23 -05:00
|
|
|
|
2023-10-26 12:35:34 -05:00
|
|
|
var app = express()
|
|
|
|
|
|
|
|
app.use(bodyParser.urlencoded({ extended: false }))
|
|
|
|
|
|
|
|
app.use(express.static(staticPath))
|
|
|
|
|
|
|
|
app.listen(PORT, () => {
|
|
|
|
console.log("Simpletube is now listening on port: " + PORT)
|
|
|
|
})
|
|
|
|
|
2023-11-27 12:32:33 -06:00
|
|
|
function resultHTML(result) {
|
|
|
|
|
|
|
|
console.log(result)
|
|
|
|
|
|
|
|
function thumbnailCheck() {
|
|
|
|
if (result.thumbnails) {
|
|
|
|
return result.thumbnails[0].url
|
|
|
|
} else {
|
|
|
|
return result.thumbnail
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function publishCheck() {
|
|
|
|
if (result.published) {
|
|
|
|
return result.published.text
|
|
|
|
} else {
|
|
|
|
return result.uploaded
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return `
|
|
|
|
<div class="col-xxl-4 col-sm-6 resultContainer">
|
|
|
|
<div class="videoResult container-fluid row">
|
|
|
|
<div class="col-lg-6 thumbparent">
|
|
|
|
<a class="videoLink" href="/watch?v=${result.id}">
|
|
|
|
<img class="thumbnail" src="${thumbnailCheck()}">
|
|
|
|
<p style="display: block; text-align: left;">${result.duration.pretty || result.durationString}</p>
|
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
<div class="col-lg-6">
|
|
|
|
<a class="videoLink" href="/watch?v=${result.id}">
|
|
|
|
<p style="font-size: 1.25rem;">${result.title || "No Title Found"}</p>
|
|
|
|
<p class="resultDescription">${(result.description || result.views.text).substring(0, 125) || "No Description"}</p>
|
|
|
|
<p style="display: block;">${publishCheck()}</p>
|
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
<div style="display: inline-block; width: 100%;">
|
|
|
|
<a style="color: white; margin: 10px; display: inline-block;" href="/channel?q=${result.channel.id}">
|
|
|
|
<img src="${result.channel.thumbnail}" class="minipfp">
|
|
|
|
${result.channel.name}
|
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
`
|
|
|
|
}
|
|
|
|
|
2023-11-19 23:52:48 -06:00
|
|
|
function msth(ms) {
|
|
|
|
var x = ms / 1000
|
|
|
|
var seconds = Math.floor(x % 60)
|
|
|
|
x /= 60
|
|
|
|
var minutes = Math.floor(x % 60)
|
|
|
|
x /= 60
|
|
|
|
var hours = Math.floor(x)
|
|
|
|
|
|
|
|
var fs = ""
|
|
|
|
|
|
|
|
if (hours >= 1) {
|
|
|
|
fs += hours
|
|
|
|
if (hours == 1) {
|
|
|
|
fs += " hour "
|
|
|
|
} else {
|
|
|
|
fs += " hours "
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (minutes >= 1) {
|
|
|
|
fs += minutes
|
|
|
|
if (minutes == 1) {
|
|
|
|
fs += " minute "
|
|
|
|
} else {
|
|
|
|
fs += " minutes "
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fs += ` ${seconds} seconds.`
|
|
|
|
|
|
|
|
return fs.trim()
|
|
|
|
}
|
|
|
|
|
2023-11-19 22:40:21 -06:00
|
|
|
async function cacher(id, quality, ready) {
|
|
|
|
vidpath = path.join(cachePath, `${id + quality}.mp4`)
|
2023-10-29 23:28:17 -05:00
|
|
|
var debounce = true
|
|
|
|
|
|
|
|
var dp = 0
|
2023-10-30 00:57:48 -05:00
|
|
|
var vidInfo = await ytdl.getBasicInfo(id)
|
2023-11-19 22:40:21 -06:00
|
|
|
|
|
|
|
if (quality == "sd") {
|
|
|
|
var video = ytdl(id, { filter: 'videoandaudio', quality: "highest", format: 'mp4' })
|
2023-10-29 23:28:17 -05:00
|
|
|
.on("progress", (chunk, ct, et) => {
|
2023-10-30 09:09:17 -05:00
|
|
|
|
2023-11-20 10:55:41 -06:00
|
|
|
if (debounce && (ct / et) > 0.1) {
|
2023-10-29 23:28:17 -05:00
|
|
|
debounce = false
|
2023-11-19 22:40:21 -06:00
|
|
|
videoCache[id + quality] = {
|
2023-10-29 23:28:17 -05:00
|
|
|
"path": vidpath,
|
|
|
|
"size": et,
|
|
|
|
"downloaded": false,
|
|
|
|
"download%": 0,
|
2023-10-30 09:09:17 -05:00
|
|
|
"lastUsed": Date.now(),
|
2023-10-30 00:57:48 -05:00
|
|
|
"duration": (vidInfo.videoDetails.lengthSeconds + 1) * 1000
|
2023-10-29 23:28:17 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
ready(vidpath, fs.readFileSync(vidpath))
|
|
|
|
}
|
|
|
|
var percent = Math.round(ct / et * 100)
|
2023-11-27 12:32:33 -06:00
|
|
|
if (!debounce) {
|
|
|
|
videoCache[id + quality]["download%"] = percent
|
|
|
|
}
|
2023-10-29 23:28:17 -05:00
|
|
|
if (!debounce && percent > dp && id in videoCache && "path" in videoCache[id]) {
|
|
|
|
dp = percent
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.on("finish", () => {
|
2023-11-27 12:32:33 -06:00
|
|
|
if ((id + quality) in videoCache) {
|
|
|
|
videoCache[id + quality]["downloaded"] = true
|
2023-10-29 23:28:17 -05:00
|
|
|
}
|
|
|
|
})
|
2023-11-19 22:40:21 -06:00
|
|
|
video.pipe(fs.createWriteStream(vidpath))
|
|
|
|
} else {
|
|
|
|
var video = ytdl(id, { filter: 'videoonly', quality: "highest" })
|
|
|
|
var audio = ytdl(id, { filter: 'audioonly', highWaterMark: 1<<25 })
|
|
|
|
|
|
|
|
const ffmpegProcess = cp.spawn(ffmpeg, [
|
|
|
|
'-i', `pipe:3`,
|
|
|
|
'-i', `pipe:4`,
|
|
|
|
'-map','0:v',
|
|
|
|
'-map','1:a',
|
|
|
|
'-c:v', 'copy',
|
|
|
|
'-c:a', 'libmp3lame',
|
|
|
|
'-crf','27',
|
|
|
|
'-preset','veryfast',
|
|
|
|
'-movflags','frag_keyframe+empty_moov',
|
|
|
|
'-f','mp4',
|
|
|
|
'-loglevel','error',
|
2023-11-19 23:52:48 -06:00
|
|
|
'-t', vidInfo.videoDetails.lengthSeconds,
|
2023-11-19 22:40:21 -06:00
|
|
|
'-'
|
|
|
|
], {
|
|
|
|
stdio: [
|
|
|
|
'pipe', 'pipe', 'pipe', 'pipe', 'pipe',
|
|
|
|
],
|
|
|
|
});
|
|
|
|
|
2023-11-19 23:52:48 -06:00
|
|
|
var debounce = false
|
|
|
|
|
|
|
|
function start(et) {
|
|
|
|
if (fs.existsSync(vidpath)) {
|
|
|
|
videoCache[id + quality] = {
|
|
|
|
"path": vidpath,
|
|
|
|
"size": et * 2,
|
|
|
|
"downloaded": true,
|
|
|
|
"download%": 0,
|
|
|
|
"lastUsed": Date.now(),
|
|
|
|
"duration": (vidInfo.videoDetails.lengthSeconds + 1) * 1000
|
|
|
|
}
|
|
|
|
ready(vidpath, fs.readFileSync(vidpath))
|
|
|
|
} else {
|
|
|
|
setTimeout(() => {
|
|
|
|
start(et)
|
|
|
|
}, 1000);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
video.on("progress", (chunk, ct, et) => {
|
|
|
|
if (!debounce) {
|
|
|
|
debounce = true
|
|
|
|
start(et)
|
|
|
|
}
|
2023-11-27 12:32:33 -06:00
|
|
|
|
2023-11-27 22:39:06 -06:00
|
|
|
videoCache[id + quality]["download%"] = Math.round(fs.statSync(vidpath).size / (et) * 100)
|
|
|
|
// console.log(Math.round(fs.statSync(vidpath).size / (et * 2) * 100))
|
2023-11-19 23:52:48 -06:00
|
|
|
})
|
|
|
|
|
|
|
|
video.pipe(ffmpegProcess.stdio[3])
|
|
|
|
audio.pipe(ffmpegProcess.stdio[4])
|
|
|
|
|
|
|
|
|
2023-11-19 22:40:21 -06:00
|
|
|
ffmpegProcess.stdio[1].pipe(fs.createWriteStream(vidpath))
|
2023-11-19 23:52:48 -06:00
|
|
|
|
2023-11-19 22:40:21 -06:00
|
|
|
.on("finish", () => {
|
|
|
|
videoCache[id + quality] = {
|
|
|
|
"path": vidpath,
|
|
|
|
"size": fs.statSync(vidpath).size,
|
|
|
|
"downloaded": true,
|
|
|
|
"download%": 100,
|
|
|
|
"lastUsed": Date.now(),
|
|
|
|
"duration": (vidInfo.videoDetails.lengthSeconds + 1) * 1000
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-10-29 23:28:17 -05:00
|
|
|
}
|
|
|
|
|
2023-11-27 22:39:06 -06:00
|
|
|
app.get("/", async (req, res) => {
|
|
|
|
var html = fs.readFileSync('resources/mainPage.html').toString()
|
|
|
|
res.send(html.substring(0, html.indexOf("{RESULTS}")))
|
|
|
|
youtube.search(" ", { type: "videos" })
|
|
|
|
.then((results) => {
|
|
|
|
addedHTML += "<h2><br>Videos:</h2>"
|
|
|
|
|
|
|
|
for (let index = 0; index < videos.length; index++) {
|
|
|
|
const result = videos[index];
|
|
|
|
addedHTML += resultHTML(result)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2023-10-28 19:08:28 -05:00
|
|
|
app.get("/search", async (req, res) => {
|
|
|
|
var search = req.query.q || "How to search on SimpleTube"
|
|
|
|
res.setHeader("Content-Type", "text/html")
|
2023-10-30 00:57:48 -05:00
|
|
|
|
|
|
|
function searchReturn(results) {
|
2023-10-28 19:08:28 -05:00
|
|
|
var videos = results.videos
|
2023-10-29 23:28:17 -05:00
|
|
|
|
2023-10-28 19:08:28 -05:00
|
|
|
var html = fs.readFileSync(searchPath).toString()
|
2023-10-29 23:28:17 -05:00
|
|
|
|
2023-10-28 19:08:28 -05:00
|
|
|
html = html.replace("{SEARCH}", search)
|
|
|
|
|
|
|
|
var addedHTML = ""
|
|
|
|
|
2023-10-29 23:28:17 -05:00
|
|
|
var channels = results.channels
|
|
|
|
if (channels.length > 0) {
|
|
|
|
|
|
|
|
addedHTML += "<h2><br>Channels:</h2>"
|
|
|
|
|
|
|
|
for (let index = 0; index < channels.length; index++) {
|
|
|
|
const channel = channels[index]
|
|
|
|
addedHTML += `
|
|
|
|
<div class="col-xxl-4 col-sm-6 resultContainer">
|
|
|
|
<div class="videoResult container-fluid row">
|
|
|
|
<div class="col-lg-5 col-md-6 thumbparent">
|
|
|
|
<a class="videoLink" href="/channel?q=${channel.id}">
|
|
|
|
<img class="pfp" src="${channel.thumbnail}">
|
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
<div class="col-lg-7 col-md-6">
|
|
|
|
<a class="videoLink" href="/channel?q=${channel.id}">
|
|
|
|
<p style="font-size: 1.25rem;">${channel.name || "No Title Found"}</p>
|
|
|
|
<p class="resultDescription">${channel.description || "No Description"}</p>
|
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-01 08:33:22 -05:00
|
|
|
if (videos.length > 0) {
|
|
|
|
addedHTML += "<h2><br>Videos:</h2>"
|
2023-10-29 23:28:17 -05:00
|
|
|
|
2023-11-01 08:33:22 -05:00
|
|
|
for (let index = 0; index < videos.length; index++) {
|
|
|
|
const result = videos[index];
|
2023-11-27 12:32:33 -06:00
|
|
|
addedHTML += resultHTML(result)
|
2023-11-01 08:33:22 -05:00
|
|
|
}
|
2023-10-28 19:08:28 -05:00
|
|
|
}
|
2023-11-01 13:15:37 -05:00
|
|
|
if (addedHTML == "") {
|
|
|
|
addedHTML = "<h2>No results found!</h2>"
|
|
|
|
}
|
2023-10-28 19:08:28 -05:00
|
|
|
res.send(html.replace("{RESULTS}", addedHTML))
|
2023-10-30 00:57:48 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
var tA = Object.keys(searchCache)
|
|
|
|
|
|
|
|
|
|
|
|
for (let index = 0; index < tA.length; index++) {
|
|
|
|
itemName = tA[index]
|
|
|
|
const item = searchCache[itemName];
|
|
|
|
|
|
|
|
if (item[1] < Date.now()) {
|
|
|
|
delete searchCache[search]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (search in searchCache) {
|
|
|
|
searchReturn(searchCache[search][0])
|
|
|
|
searchCache[search][1] = Date.now() + searchCacheDur
|
|
|
|
} else {
|
|
|
|
youtube.search(search, { type: "all" })
|
2023-10-30 09:09:17 -05:00
|
|
|
.then((result) => {
|
|
|
|
searchReturn(result)
|
|
|
|
searchCache[search] = [result, Date.now() + searchCacheDur]
|
|
|
|
})
|
2023-10-30 00:57:48 -05:00
|
|
|
}
|
2023-10-28 19:08:28 -05:00
|
|
|
})
|
|
|
|
|
2023-11-01 08:33:22 -05:00
|
|
|
app.get("/channel", async (req, res) => {
|
|
|
|
var section = req.query.s || "videos"
|
|
|
|
var channel = req.query.q || "UChcrBJNJLZucy3TPyGyAY2g"
|
|
|
|
|
|
|
|
var html = fs.readFileSync(channelPath).toString()
|
|
|
|
|
|
|
|
var info = await ytExt.channelInfo(channel, { includeVideos: true })
|
|
|
|
|
|
|
|
var videos = info.videos
|
|
|
|
var addedHTML = ""
|
|
|
|
if (section == "videos") {
|
|
|
|
for (let index = 0; index < videos.length; index++) {
|
|
|
|
const result = videos[index];
|
|
|
|
if (result.title != undefined) {
|
2023-11-27 12:32:33 -06:00
|
|
|
addedHTML += resultHTML(result)
|
2023-11-01 08:33:22 -05:00
|
|
|
} else {
|
2023-11-17 11:58:39 -06:00
|
|
|
addedHTML = "<p>Failed to load.</p>"
|
2023-11-01 08:33:22 -05:00
|
|
|
}
|
2023-11-19 22:40:21 -06:00
|
|
|
|
2023-11-01 08:33:22 -05:00
|
|
|
}
|
|
|
|
} else if (section == "about") {
|
|
|
|
addedHTML += `
|
|
|
|
<div id="description">
|
|
|
|
<h2>Description: <br></h2>
|
|
|
|
<p>${info.description}</p>
|
|
|
|
<p>Subscribers: ${info.subscribers.pretty}
|
|
|
|
</div>
|
|
|
|
`
|
|
|
|
}
|
|
|
|
|
|
|
|
if (addedHTML == "") {
|
|
|
|
addedHTML = `<h2>Failed to load ${section}! Sorry... T^T<h2>`
|
|
|
|
}
|
|
|
|
|
|
|
|
html = html.replace("{RESULTS}", addedHTML)
|
|
|
|
|
|
|
|
html = html.replace("{CHANNEL_NAME}", info.name)
|
|
|
|
html = html.replace("{CHANNEL_DESC}", info.description)
|
|
|
|
|
|
|
|
for (let index = 0; index < 3; index++) {
|
|
|
|
html = html.replace("{CHANNEL_ID}", info.id)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
html = html.replace("{CHANNEL_PFP}", info.thumbnails[0].url)
|
|
|
|
html = html.replace("{SUB_COUNT}", info.subscribers.pretty)
|
|
|
|
|
|
|
|
res.send(html)
|
|
|
|
})
|
|
|
|
|
2023-10-26 12:35:34 -05:00
|
|
|
app.get("/video", async (req, res) => {
|
|
|
|
var id = req.query.q || req.query.v
|
2023-11-19 22:40:21 -06:00
|
|
|
var quality = req.query.quality
|
2023-10-26 12:35:34 -05:00
|
|
|
var range = req.headers.range
|
|
|
|
|
|
|
|
res.setHeader("X-Accel-Buffering", "no")
|
2023-11-01 13:10:45 -05:00
|
|
|
res.setHeader("Content-Type", "video/mp4")
|
2023-10-26 12:35:34 -05:00
|
|
|
|
2023-10-27 00:38:23 -05:00
|
|
|
if (ytdl.validateURL(id)) {
|
|
|
|
id = ytdl.getVideoID(id)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!ytdl.validateID(id)) {
|
2023-10-26 12:35:34 -05:00
|
|
|
res.setHeader("Content-Type", "text/html")
|
|
|
|
res.write("Not a valid video id or url!")
|
|
|
|
res.end()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (range) {
|
2023-10-27 00:38:23 -05:00
|
|
|
function ready(vidpath) {
|
2023-11-27 22:39:06 -06:00
|
|
|
if (fs.existsSync(vidpath) && fs.statSync(vidpath).size > 1) {
|
2023-11-27 12:32:33 -06:00
|
|
|
const fileSize = fs.statSync(vidpath).size
|
2023-10-29 23:28:17 -05:00
|
|
|
const parts = range.replace(/bytes=/, "").split("-")
|
|
|
|
const start = parseInt(parts[0], 10)
|
|
|
|
const end = parts[1]
|
|
|
|
? parseInt(parts[1], 10)
|
|
|
|
: fileSize - 1
|
2023-10-30 09:09:17 -05:00
|
|
|
|
2023-10-29 23:28:17 -05:00
|
|
|
if (start >= fs.statSync(vidpath).size + 1) {
|
|
|
|
return
|
|
|
|
}
|
2023-10-30 09:09:17 -05:00
|
|
|
|
2023-10-29 23:28:17 -05:00
|
|
|
const chunksize = (end - start) + 1
|
2023-10-30 09:09:17 -05:00
|
|
|
|
2023-10-29 23:28:17 -05:00
|
|
|
const head = {
|
|
|
|
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
|
|
|
'Accept-Ranges': 'bytes',
|
|
|
|
'Content-Length': chunksize,
|
|
|
|
'Content-Type': 'video/mp4',
|
|
|
|
}
|
2023-10-30 09:09:17 -05:00
|
|
|
|
2023-10-29 23:28:17 -05:00
|
|
|
res.writeHead(206, head)
|
2023-10-30 09:09:17 -05:00
|
|
|
|
2023-11-27 22:39:06 -06:00
|
|
|
if (end == -1) {
|
|
|
|
fs.createReadStream(vidpath, { start: start }).pipe(res)
|
|
|
|
} else {
|
|
|
|
fs.createReadStream(vidpath, { start: start, end: end }).pipe(res)
|
|
|
|
}
|
2023-10-27 00:38:23 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-19 22:40:21 -06:00
|
|
|
if ((id + quality) in videoCache) {
|
|
|
|
id = id + quality
|
2023-10-29 23:28:17 -05:00
|
|
|
if ("path" in videoCache[id]) {
|
|
|
|
ready(videoCache[id].path)
|
2023-10-30 09:09:17 -05:00
|
|
|
videoCache[id].lastUsed = Date.now()
|
2023-10-29 23:28:17 -05:00
|
|
|
}
|
2023-10-27 00:38:23 -05:00
|
|
|
} else {
|
2023-11-27 12:32:33 -06:00
|
|
|
videoCache[id + quality] = []
|
2023-11-19 22:40:21 -06:00
|
|
|
cacher(id, quality, ready)
|
2023-10-27 00:38:23 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-10-26 12:35:34 -05:00
|
|
|
|
|
|
|
} else {
|
|
|
|
const head = {
|
|
|
|
'Content-Length': fileSize,
|
|
|
|
'Content-Type': 'video/mp4',
|
|
|
|
}
|
|
|
|
res.writeHead(200, head)
|
|
|
|
fs.createReadStream(path).pipe(res)
|
|
|
|
}
|
2023-10-30 09:09:17 -05:00
|
|
|
|
|
|
|
var tA = Object.keys(videoCache)
|
|
|
|
for (let index = 0; index < tA.length; index++) {
|
|
|
|
const itemName = tA[index];
|
|
|
|
const item = videoCache[itemName]
|
|
|
|
const itemPath = item.path
|
|
|
|
|
|
|
|
if ("lastUsed" in item && item.lastUsed + (item.duration * 2.5) < Date.now()) {
|
|
|
|
delete videoCache[itemName]
|
|
|
|
if (fs.existsSync(itemPath)) {
|
|
|
|
fs.unlinkSync(itemPath)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-10-26 12:35:34 -05:00
|
|
|
})
|
|
|
|
|
|
|
|
app.get("/watch", async (req, res) => {
|
2023-10-28 19:08:28 -05:00
|
|
|
var id = req.query.q || req.query.v || "ubFq-wV3Eic"
|
2023-11-27 12:32:33 -06:00
|
|
|
var quality = req.query.quality || "hd"
|
2023-10-26 12:35:34 -05:00
|
|
|
|
|
|
|
res.setHeader("Content-Type", "text/html")
|
|
|
|
|
2023-10-27 00:38:23 -05:00
|
|
|
if (ytdl.validateURL(id)) {
|
|
|
|
id = ytdl.getVideoID(id)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!ytdl.validateID(id)) {
|
2023-11-20 10:55:41 -06:00
|
|
|
res.write(cssHeader)
|
|
|
|
res.write(topBar)
|
|
|
|
res.write("<h2>Not a valid video id or url!</h2>")
|
2023-10-27 00:38:23 -05:00
|
|
|
res.end()
|
|
|
|
return
|
|
|
|
}
|
2023-11-20 10:55:41 -06:00
|
|
|
|
|
|
|
try {
|
|
|
|
var vidInfo = (await ytdl.getBasicInfo(id)).videoDetails
|
|
|
|
} catch (error) {
|
|
|
|
res.write(cssHeader)
|
|
|
|
res.write(topBar)
|
|
|
|
res.write("<h2>Failed to get video info! This likely means the video is age restricted, or deleted.</h2>", () => {res.end()})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-10-27 00:38:23 -05:00
|
|
|
|
2023-10-26 12:35:34 -05:00
|
|
|
var html = fs.readFileSync(playerPath).toString()
|
|
|
|
|
2023-11-19 22:40:21 -06:00
|
|
|
if (quality == "sd") {
|
|
|
|
html = html.replace(`href="?quality=sd&q={VIDEOID}"`, `style="color: white;"`)
|
|
|
|
} else {
|
|
|
|
html = html.replace(`href="?quality=hd&q={VIDEOID}"`, `style="color: white;"`)
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let index = 0; index < 2; index++) {
|
|
|
|
html = html.replace("{VIDEOID}", id)
|
|
|
|
}
|
|
|
|
|
|
|
|
html = html.replace("{VIDEO_QUALITY}", quality)
|
2023-10-26 12:35:34 -05:00
|
|
|
|
2023-11-19 23:52:48 -06:00
|
|
|
html = html.replace("{DURATION}", msth(vidInfo.lengthSeconds * 1000))
|
|
|
|
|
2023-10-26 12:35:34 -05:00
|
|
|
html = html.replace("{CSS_HEADER}", cssHeader)
|
|
|
|
|
2023-10-28 19:08:28 -05:00
|
|
|
for (let index = 0; index < 2; index++) {
|
|
|
|
html = html.replace("{VIDEO_TITLE}", vidInfo.title)
|
2023-10-29 23:28:17 -05:00
|
|
|
|
2023-10-28 19:08:28 -05:00
|
|
|
}
|
2023-10-26 12:35:34 -05:00
|
|
|
|
|
|
|
html = html.replace("{VIDEO_DESCRIPTION}", vidInfo.description || "No Description.")
|
2023-11-27 12:32:33 -06:00
|
|
|
console.log(videoCache)
|
|
|
|
if (!((id + quality) in videoCache && videoCache[id + quality].downloaded == true)) {
|
2023-10-27 00:38:23 -05:00
|
|
|
html = html.replace("{CACHE_WARNING}", `
|
|
|
|
<p style="color: lightgray">Please note that this video has not been fully cached, and may have trouble loading!
|
2023-10-29 00:01:40 -05:00
|
|
|
<br>{DOWNLOAD_PERCENT}% cached as of page load. If content fails to load after a minute, reload the page!</p>
|
2023-10-27 00:38:23 -05:00
|
|
|
`)
|
2023-11-27 12:32:33 -06:00
|
|
|
if ((id + quality) in videoCache && "download%" in videoCache[id + quality]) {
|
|
|
|
html = html.replace("{DOWNLOAD_PERCENT}", videoCache[id + quality]["download%"])
|
2023-10-27 00:38:23 -05:00
|
|
|
} else {
|
|
|
|
html = html.replace("{DOWNLOAD_PERCENT}", "0")
|
|
|
|
}
|
|
|
|
} else {
|
2023-10-27 00:44:46 -05:00
|
|
|
html = html.replace("{CACHE_WARNING}", "<p>This video is fully cached!</p>")
|
2023-10-27 00:38:23 -05:00
|
|
|
}
|
|
|
|
|
2023-10-26 12:35:34 -05:00
|
|
|
var finalThumb = vidInfo.thumbnails[vidInfo.thumbnails.length - 1].url
|
|
|
|
html = html.replace("{VIDEO_THUMBNAIL}", finalThumb)
|
|
|
|
|
|
|
|
res.send(html)
|
|
|
|
})
|
|
|
|
|
2023-11-27 22:39:06 -06:00
|
|
|
process.on('uncaughtException', (err, origin) => {
|
|
|
|
fs.writeSync(
|
|
|
|
process.stderr.fd,
|
|
|
|
`Caught exception: ${err}\n` +
|
|
|
|
`Exception origin: ${origin}`,
|
|
|
|
);
|
|
|
|
});
|