commit 80411aeb08d3899c0fbb6afbd9eb178e10ca6526 Author: aandrew-me Date: Mon Jul 25 22:15:38 2022 +0600 Initial version diff --git a/.directory b/.directory new file mode 100644 index 0000000..2607824 --- /dev/null +++ b/.directory @@ -0,0 +1,2 @@ +[Desktop Entry] +Icon=/home/andrew/Pictures/icons/icon.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c04b22c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +release +package-lock.json +test.js diff --git a/app.js b/app.js new file mode 100644 index 0000000..bbbcb2c --- /dev/null +++ b/app.js @@ -0,0 +1,200 @@ +const fs = require("fs"); +const ytdl = require("ytdl-core"); +const express = require("express"); +const app = express(); +const bodyParser = require("body-parser"); +const ffmpeg = require("ffmpeg-static"); +const os = require("os"); +const cp = require("child_process"); + +app.use(bodyParser.urlencoded({ extended: true })); +app.use(express.static(__dirname + "/public")); + +// To do +// Preset +// Download location selection +// Choosing correct encoding + + +app.get("/", (req, res) => { + res.sendFile(__dirname + "/index.html"); +}); + +async function getVideoInfo(url) { + const info = await ytdl.getInfo(url); + return info; +} + +app.post("/", (req, res) => { + const url = req.body.url; + getVideoInfo(url) + .then((video) => + res.json({ + status: true, + title: video.videoDetails.title, + formats: video.formats, + url: url, + }) + ) + .catch((error) => + res.json({ status: false, message: "Use correct URL" }) + ); +}); + +// Downloading video + +app.post("/download", (req, res) => { + const itag = parseInt(req.body.audioTag || req.body.videoTag); + const url = req.body.url; + + + // Function to find video/audio info + + async function findInfo(url, itag) { + const data = await ytdl.getInfo(url); + const format = ytdl.chooseFormat(data.formats, { quality: itag }); + const title = data.videoDetails.title; + let extension; + if (format.hasVideo) { + extension = format.mimeType.split("; ")[0].split("/")[1]; + } else { + if (format.audioCodec === "mp4a.40.2") { + extension = "m4a"; + } else { + extension = format.audioCodec; + } + } + + let quality; + if (format.hasVideo) { + quality = format.qualityLabel; + } else { + quality = format.audioBitrate + "kbps"; + } + const filename = title + "_" + quality + "." + extension + const info = { + format: format, + title: title, + extension: extension, + quality:quality, + filename:filename + }; + return info; + } + + findInfo(url, itag).then((info) => { + const format = info.format; + const filename = info.filename; + + // If video + if (format.hasVideo) { + let video = ytdl(url, { quality: itag }); + let audio = ytdl(url, { + 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", + "fast", + "-movflags", + "frag_keyframe+empty_moov", + '-f', "mp4", + "-loglevel", + "error", + "-", + ], + { + stdio: ["pipe", "pipe", "pipe", "pipe", "pipe"], + } + ); + + video.pipe(ffmpegProcess.stdio[3]); + audio.pipe(ffmpegProcess.stdio[4]); + + res.header("Content-Disposition", "attachment; filename='" + filename + "'"); + ffmpegProcess.stdio[1].pipe(res); + + } + // If audio + else { + res.header("Content-Disposition", "attachment; filename=" + filename); + + ytdl(url, { quality: itag }) + .on("progress", (_, downloaded, size) => { + const progress = (downloaded / size) * 100; + }) + // .pipe(fs.createWriteStream(os.homedir() + "/Downloads/" + filename)) + .pipe(res); + } + }); +}); + +// Off for now +app.post("", (req, res) => { + const itag = parseInt(req.body.audioTag || req.body.videoTag); + const url = req.body.url; + + async function findFormat(url, itag) { + const info = await ytdl.getInfo(url); + const format = ytdl.chooseFormat(info.formats, { quality: itag }); + const title = info.videoDetails.title; + + // Quality for filename + let quality; + if (format.hasVideo) { + quality = format.qualityLabel; + } else { + quality = format.audioBitrate + "kbps"; + } + + // File extension for filename + let extension; + if (format.hasVideo) { + extension = format.mimeType.split("; ")[0].split("/")[1]; + } else { + if (format.audioCodec === "mp4a.40.2") { + extension = "m4a"; + } else { + extension = format.audioCodec; + } + } + const filename = title + "_" + quality + "." + extension; + return filename; + } + + findFormat(url, itag) + .then((filename) => { + res.header("Content-Disposition", "attachment; filename=.m4a"); + ytdl(url, { quality: itag }) + .on("progress", (_, downloaded, size) => { + const progress = (downloaded / size) * 100; + // console.log("Progress: " + progress + "%" ) + }) + // .pipe(fs.createWriteStream(os.homedir() + "/Downloads/" + filename)) + .pipe(res); + }) + .catch((error) => { + console.log(error); + }); +}); + +app.listen(3000, () => { + console.log("Server: http://localhost:3000"); +}); diff --git a/index.html b/index.html new file mode 100644 index 0000000..f97f280 --- /dev/null +++ b/index.html @@ -0,0 +1,75 @@ + + + + + + + + Youtube Video Downloader + + + + + + +
+
+
+ +

YouTube Downloader

+ + + + +

+ +
+ Loading + + + + + + + + + + + +
+ + +
+
+ + +
+

Title:

+ +
+
+ + +
+ + +
+
+ +
+
+ + +
+ + +
+
+ +
+ + + \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..93072ac --- /dev/null +++ b/main.js @@ -0,0 +1,28 @@ +const { app, BrowserWindow } = require('electron') +const path = require('path') +require("./app.js") + +function createWindow () { + const win = new BrowserWindow({ + width: 800, + height: 600 + }) + + win.loadURL("http://localhost:3000") +} + +app.whenReady().then(() => { + createWindow() + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } + }) +}) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..9efb0e8 --- /dev/null +++ b/package.json @@ -0,0 +1,68 @@ +{ + "dependencies": { + "body-parser": "^1.20.0", + "express": "^4.18.1", + "ffmpeg-static": "^5.0.2", + "ytdl-core": "^4.11.0" + }, + "name": "ytdownloader", + "version": "1.0.0", + "main": "main.js", + "scripts": { + "start": "electron .", + "windows": "electron-builder -w", + "linux": "electron-builder -l", + "mac": "electron-builder -m" + }, + "keywords": [], + "author": "Andrew", + "license": "MIT", + "description": "Download videos and audios from YouTube", + "devDependencies": { + "electron": "^19.0.9", + "electron-builder": "^23.1.0" + }, + "build": { + "productName": "YTDownloader", + "appId": "com.andrew.ytdownloader", + "mac": { + "category": "Utility", + "target": [ + "dmg" + ] + }, + "dmg": { + "contents": [ + { + "x": 130, + "y": 220 + }, + { + "x": 410, + "y": 220, + "type": "link", + "path": "/Applications" + } + ], + "sign": false + }, + "asar": true, + "directories": { + "buildResources": "resources", + "output": "release" + }, + "linux": { + "target": [ + "AppImage" + ], + "category": "Utility" + }, + "win": { + "target": "nsis" + }, + "nsis": { + "allowToChangeInstallationDirectory": true, + "oneClick": false + } + } +} diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000..12d0d92 Binary files /dev/null and b/public/icon.png differ diff --git a/public/index.css b/public/index.css new file mode 100644 index 0000000..ff94137 --- /dev/null +++ b/public/index.css @@ -0,0 +1,149 @@ +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", + sans-serif; + text-align: center; + font-size: xx-large; + padding: 10px; + background-color: whitesmoke; +} + +#title { + font-size: x-large; + font-family: sans-serif; +} + +h1 { + font-size: 50px; +} + +input[type="text"] { + padding: 10px; + border-radius: 10px; + border: 1px solid gray; + outline: none; + font-size: large; + width: 70%; +} + +#getInfo { + padding: 10px; + background-color: rgb(64, 227, 64); + border: none; + border-radius: 8px; + color: white; + font-size: large; + cursor: pointer; + + /* animation-name: clickAnimation; */ + animation-duration: .5s; + animation-timing-function: linear; +} + +@keyframes clickAnimation { + 0% {background-color: rgb(64, 227, 64);} + 50% {background-color: rgb(40, 126, 40);} + 100% {background-color: rgb(64, 227, 64);} +} + +#hidden { + display: none; + /* display: inline-block; */ + background-color: rgb(143, 239, 207); + border-radius: 10px; + width: 80%; + padding: 10px; + color:black; +} + +#btnContainer { + display: flex; + flex-direction: row; + justify-content: center; +} + +.toggleBtn { + width: 50%; + font-size: x-large; + border: none; + background-color: rgb(108, 231, 190); + border-radius: 10px; + cursor: pointer; + padding: 8px; +} + +select { + padding: 15px; + background-color: lightgreen; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: large; + margin: 8px; + outline:none; +} + +label { + position: relative; + top: 3px; +} + +#videoList, +#audioList { + display: none; +} + +.submitBtn { + padding: 15px; + margin: 15px; + border-radius: 8px; + background-color: rgb(64, 227, 64); + color: white; + border: none; + font-size: large; + cursor: pointer; + + animation-duration: .5s; + animation-timing-function: linear; +} + +#incorrectMsg { + color: rgb(250, 59, 59); +} + +#loadingWrapper{ + display:none; + flex-direction: row; + justify-content: center; + align-items: center; +} + +svg { + width: 100px; + height: 100px; + display: inline-block; + margin-left:20px + } + +#themeToggle { + width: 55px; + height: 30px; + background-color: rgb(147, 174, 185); + border-radius: 40px; + display: flex; + cursor: pointer; + transition: linear; + transition-duration: 0.4s; +} + +#themeToggleInside { + background-color: rgb(255, 255, 255); + border-radius: 30px; + width: 22px; + height: 22px; + margin: 4px; + position: relative; + transition: linear; + transition-duration: 0.4s; + left:0px; +} diff --git a/public/index.js b/public/index.js new file mode 100644 index 0000000..1a84e29 --- /dev/null +++ b/public/index.js @@ -0,0 +1,170 @@ +const videoToggle = document.getElementById("videoToggle"); +const audioToggle = document.getElementById("audioToggle"); +const incorrectMsg = document.getElementById("incorrectMsg"); +const loadingMsg = document.getElementById("loadingWrapper") + +function getInfo() { + incorrectMsg.textContent = ""; + loadingMsg.style.display = "flex"; + const url = document.getElementById("url").value; + const options = { + method: "POST", + body: "url=" + url, + headers: { + "Content-Type": "Application/x-www-form-urlencoded", + }, + }; + + fetch("/", options) + .then((response) => response.json()) + .then((data) => { + console.log(data.formats); + + const urlElements = document.querySelectorAll(".url"); + + urlElements.forEach((element) => { + element.value = data.url; + }); + + if (data.status == true) { + loadingMsg.style.display = "none"; + document.getElementById("hidden").style.display = + "inline-block"; + document.getElementById("title").innerHTML = + "Title: " + data.title; + document.getElementById("videoList").style.display = "block"; + videoToggle.style.backgroundColor = "rgb(67, 212, 164)"; + + for (let format of data.formats) { + let sizeSplitted = (Number(format.contentLength) / 1000000) + .toString().split(".") + let size = sizeSplitted[0] + sizeSplitted[1].slice(0,1) + + size = size + " MB"; + + // For videos + if (format.hasVideo && format.contentLength && format.container == "mp4") { + + const itag = format.itag; + const element = + ""; + document.getElementById( + "videoFormatSelect" + ).innerHTML += element; + + } + + // For audios + else { + const pattern = /^mp*4a[0-9.]+$/g; + let audioCodec; + const itag = format.itag; + + if (pattern.test(format.audioCodec)) { + audioCodec = "mp4a"; + } else { + audioCodec = format.audioCodec; + } + const element = + ""; + document.getElementById( + "audioFormatSelect" + ).innerHTML += element; + } + } + } else { + loadingMsg.style.display = "none"; + incorrectMsg.textContent = "Some error has occured"; + } + }) + .catch((error) => { + if (error) { + loadingMsg.style.display = "none"; + incorrectMsg.textContent = error; + } + }); +} + +document.getElementById("getInfo").addEventListener("click", (event) => { + getInfo(); +}); + +document.getElementById("url").addEventListener("keypress", (event) => { + if (event.key == "Enter") { + getInfo(); + } +}); + +videoToggle.addEventListener("click", (event) => { + videoToggle.style.backgroundColor = "rgb(67, 212, 164)"; + audioToggle.style.backgroundColor = "rgb(108, 231, 190)"; + document.getElementById("audioList").style.display = "none"; + document.getElementById("videoList").style.display = "block"; +}); + +audioToggle.addEventListener("click", (event) => { + audioToggle.style.backgroundColor = "rgb(67, 212, 164)"; + videoToggle.style.backgroundColor = "rgb(108, 231, 190)"; + document.getElementById("videoList").style.display = "none"; + document.getElementById("audioList").style.display = "block"; +}); + +// Toggle theme +let darkTheme = false; +let button = document.getElementById("themeToggle"); +let circle = document.getElementById("themeToggleInside"); +function toggle() { + if (darkTheme == false) { + circle.style.left = "25px"; + button.style.backgroundColor = "rgb(80, 193, 238)"; + darkTheme = true; + document.body.style.backgroundColor = "rgb(50,50,50)"; + document.getElementById("hidden").style.backgroundColor = "rgb(143, 239, 207)" + document.body.style.color = "whitesmoke"; + localStorage.setItem("theme", "dark"); + } else { + circle.style.left = "0px"; + darkTheme = false; + button.style.backgroundColor = "rgb(147, 174, 185)"; + document.body.style.backgroundColor = "whitesmoke"; + document.getElementById("hidden").style.backgroundColor = "rgb(203, 253, 236)" + document.body.style.color = "black"; + localStorage.setItem("theme", "light"); + } +} + +const storageTheme = localStorage.getItem("theme"); + +if (storageTheme == "dark") { + darkTheme = false; + toggle(); +} else if (storageTheme == "light") { + darkTheme = true; + toggle(); +} +//// + +function clickAnimation(id) { + document.getElementById(id).style.animationName = "clickAnimation"; + + setTimeout(() => { + document.getElementById("getInfo").style.animationName = ""; + }, 500); +} diff --git a/resources/icon.icns b/resources/icon.icns new file mode 100644 index 0000000..6b8e2b4 Binary files /dev/null and b/resources/icon.icns differ diff --git a/resources/icon.ico b/resources/icon.ico new file mode 100644 index 0000000..366c4cb Binary files /dev/null and b/resources/icon.ico differ diff --git a/resources/icon.png b/resources/icon.png new file mode 100644 index 0000000..12d0d92 Binary files /dev/null and b/resources/icon.png differ