diff --git a/.gitignore b/.gitignore index c6bba59..f5e3ff0 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,6 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# Cache +cache \ No newline at end of file diff --git a/index.js b/index.js index dca4773..193039f 100644 --- a/index.js +++ b/index.js @@ -2,18 +2,33 @@ const express = require("express"), path = require("path"), fs = require("fs"), ytdl = require("ytdl-core"), - bodyParser = require("body-parser") + bodyParser = require("body-parser"), + youtube = require("scrape-youtube") const PORT = process.env.PORT || 8080 const staticPath = path.join(__dirname, 'static') + +const cssPath = path.join(staticPath, 'mainStyle.css') + const resources = path.join(__dirname, 'resources') +const cachePath = path.join(__dirname, 'cache') + const playerPath = path.join(resources, 'player.html') -const cssPath = path.join(resources, 'mainStyle.css') const cssHeader = `` + + +if (fs.existsSync(cachePath)) { + fs.rmSync(cachePath, { recursive: true, force: true }) +} + +fs.mkdirSync(cachePath) + +var videoCache = {} + var app = express() app.use(bodyParser.urlencoded({ extended: false })) @@ -25,13 +40,17 @@ app.listen(PORT, () => { }) app.get("/video", async (req, res) => { + var id = req.query.q || req.query.v var range = req.headers.range - console.log(req.headers.range) res.setHeader("X-Accel-Buffering", "no") - if (!ytdl.validateID(id) && !ytdl.validateURL(id)) { + if (ytdl.validateURL(id)) { + id = ytdl.getVideoID(id) + } + + if (!ytdl.validateID(id)) { res.setHeader("Content-Type", "text/html") res.write("Not a valid video id or url!") res.end() @@ -41,31 +60,69 @@ app.get("/video", async (req, res) => { res.setHeader("Content-Type", "video/mp4") if (range) { - const video = ytdl(id, { format: 'mp4' }) - video.on("info", (vidinfo, dlinfo) => { - - const fileSize = dlinfo.contentLength + function ready(vidpath) { + const fileSize = videoCache[id].size + // const fileSize = fs.statSync(vidpath).size + 1 const parts = range.replace(/bytes=/, "").split("-") const start = parseInt(parts[0], 10) const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1 - - if (start >= fileSize) { + + if (start >= fs.statSync(vidpath).size + 1) { + console.log("AAAAAAAAA") res.status(416).send('Requested range not satisfiable\n' + start + ' >= ' + fileSize); return } const chunksize = (end - start) + 1 - // const chunksize = 6585810944 - // console.log(start, end) - res.setHeader("Content-Range", `bytes ${start}-${end}/${fileSize}`) - res.setHeader("Accept-Ranges", 'bytes') - res.setHeader("Content-Length", chunksize) + const head = { + 'Content-Range': `bytes ${start}-${end}/${fileSize}`, + 'Accept-Ranges': 'bytes', + 'Content-Length': chunksize, + 'Content-Type': 'video/mp4', + } + + res.writeHead(206, head) + + fs.createReadStream(vidpath, { start: start }).pipe(res) + } + + if (id in videoCache) { + ready(videoCache[id].path) + } else { + vidpath = path.join(__dirname, `cache/${id}.mp4`) + + var debounce = true + + var dp = 0 + ytdl(id, { filter: "videoandaudio", quality: "highest", format: 'mp4' }) + .on("progress", (chunk, ct, et) => { + if (debounce) { + debounce = false + videoCache[id] = { + "path": vidpath, + "size": et, + "downloaded": false, + "download%": 0 + } + ready(vidpath, fs.readFileSync(vidpath)) + } + var percent = Math.round(ct / et * 100) + if (percent > dp) { + dp = percent + videoCache[id]["download%"] = dp + } + }) + .on("finish", () => { + videoCache[id]["downloaded"] = true + }) + .pipe(fs.createWriteStream(vidpath)) + } + + - video.pipe(res) - }) } else { const head = { 'Content-Length': fileSize, @@ -77,10 +134,21 @@ app.get("/video", async (req, res) => { }) app.get("/watch", async (req, res) => { - const id = req.query.q || req.query.v + var id = req.query.q || req.query.v res.setHeader("Content-Type", "text/html") + if (ytdl.validateURL(id)) { + id = ytdl.getVideoID(id) + } + + if (!ytdl.validateID(id)) { + res.setHeader("Content-Type", "text/html") + res.write("Not a valid video id or url!") + res.end() + return + } + var vidInfo = (await ytdl.getBasicInfo(id)).videoDetails var html = fs.readFileSync(playerPath).toString() @@ -93,6 +161,20 @@ app.get("/watch", async (req, res) => { html = html.replace("{VIDEO_DESCRIPTION}", vidInfo.description || "No Description.") + if (!(id in videoCache && videoCache[id]["downloaded"] == true)) { + html = html.replace("{CACHE_WARNING}", ` +

Please note that this video has not been fully cached, and may have trouble loading! +
{DOWNLOAD_PERCENT}% cached.

+ `) + if (id in videoCache && "download%" in videoCache[id]) { + html = html.replace("{DOWNLOAD_PERCENT}", videoCache[id]["download%"]) + } else { + html = html.replace("{DOWNLOAD_PERCENT}", "0") + } + } else { + html = html.replace("{CACHE_WARNING}", "") + } + var finalThumb = vidInfo.thumbnails[vidInfo.thumbnails.length - 1].url html = html.replace("{VIDEO_THUMBNAIL}", finalThumb) diff --git a/package-lock.json b/package-lock.json index 88de371..af9cc52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "body-parser": "^1.20.2", "express": "^4.18.2", + "scrape-youtube": "^2.4.0", "ytdl-core": "^4.11.5" } }, @@ -591,6 +592,11 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" }, + "node_modules/scrape-youtube": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/scrape-youtube/-/scrape-youtube-2.4.0.tgz", + "integrity": "sha512-fUmtg2Fa8xKSGW3S7BvQwaHGxeFUeTtIsHU/AEQcBYQfCcJfDIVJeX1jtBuPOYqy3VaBVDqwdCpSYfMIHjGDEQ==" + }, "node_modules/send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", diff --git a/package.json b/package.json index 4ab4b3e..8cc35e5 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dependencies": { "body-parser": "^1.20.2", "express": "^4.18.2", + "scrape-youtube": "^2.4.0", "ytdl-core": "^4.11.5" } } diff --git a/resources/mainStyle.css b/resources/mainStyle.css deleted file mode 100644 index b140156..0000000 --- a/resources/mainStyle.css +++ /dev/null @@ -1,20 +0,0 @@ -body { - height: 100vh; -} - -#videoPlayer { - margin: auto; - padding: 0; - object-fit: contain; - display: block; - height: 100%; - max-width: 100%; - /* height: 80vh; */ -} - -#videoContainer { - height: 80%; - max-width: 100vw; - display: flex; - background-color: black; -} \ No newline at end of file diff --git a/resources/player.html b/resources/player.html index ab64676..9197aab 100644 --- a/resources/player.html +++ b/resources/player.html @@ -1,5 +1,6 @@ + @@ -8,14 +9,25 @@ Document + +
+

Simpletube

+
-

-

{VIDEO_TITLE}

-

{VIDEO_DESCRIPTION}

+
+

{VIDEO_TITLE}

+ {CACHE_WARNING} +
+

Description:

+

{VIDEO_DESCRIPTION}

+
+
+ \ No newline at end of file diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..684c998 --- /dev/null +++ b/static/index.html @@ -0,0 +1,33 @@ + + + + + + + + + + SimpleTube + + + +
+

SimpleTube

+
+
+

Welcome to SimpleTube

+

SimpleTube is a Youtube client that aims to be free of Javascript, as small as possible, and lack annoyances. +
+ Currently SimpleTube does not have a trending tab or anything, so please use the search function at the top! +
+ (Sorry!)

+

More about the project:

+

+ Github + The developer +

+ +
+ + + \ No newline at end of file diff --git a/static/mainStyle.css b/static/mainStyle.css new file mode 100644 index 0000000..cbb3c4c --- /dev/null +++ b/static/mainStyle.css @@ -0,0 +1,79 @@ +body { + height: 100vh; + padding: 0; + margin: 0; + background-color: rgb(40, 30, 50); +} + +* { + color: white; + font-family: Verdana, Geneva, Tahoma, sans-serif; + line-height: 2.5rem; +} + +a:not(h1 > a) { + font-size: 1.5rem; + background-color: black; + padding: 8px; + margin: 5px; + border: 2px white solid; + border-radius: 15px; + color: rgb(240, 220, 255); + text-decoration: none; +} + +h1:not(#titleBar > h1), h2, p, a { + text-align: center; +} + +#title { + padding: 10px; +} + +#titleBar { + background-color: rgb(5, 0, 10); + /* border: 0px; */ + border: 2px transparent solid; + border-bottom-color: white; + position: sticky; + top: 0px; +} + +#titleBar > h1 { + margin: 10px; +} + +#titleBar > h1 > a { + text-decoration: none; +} + +#description { + background-color: rgb(12, 10, 15); + padding: 10px; + border: 2px gray solid; +} + +#videoPlayer { + margin: auto; + padding: 0; + object-fit: contain; + display: block; + /* height: 100%; + max-width: 100%; */ + width: 100%; + aspect-ratio: 1.8/1; + max-height: 80vh; + /* height: 80vh; */ +} + +#videoContainer { + max-width: 100vw; + padding: 0; + margin: 0; + display: flex; + background-color: black; +} + +p { + padding: 5px; +} \ No newline at end of file