Major backend changes and some UX improvements

pull/9/head
aandrew-me 4 years ago
parent 6040260c0c
commit aca0efeabe

357
app.js

@ -1,357 +0,0 @@
const express = require("express");
const app = express();
const http = require("http");
const server = http.createServer(app);
const { Server } = require("socket.io");
const io = new Server(server);
const fs = require("fs");
const ytdl = require("ytdl-core");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const ffmpeg = require("ffmpeg-static");
const cp = require("child_process");
const os = require("os");
// Directories
const homedir = os.homedir();
const appdir = homedir + "/.ytDownloader/";
const tempDir = appdir + "temp/";
const configPath = appdir + "config.json";
fs.mkdirSync(homedir + "/.ytDownloader/temp", { recursive: true });
let config;
// Download directory
let downloadDir = "";
// Handling config file
fs.readFile(configPath, (err) => {
if (err) {
fs.writeFileSync(configPath, "{}");
}
try {
config = require(configPath);
} catch (error) {
// If config file is not in correct format
fs.writeFileSync(configPath, "{}");
config = require(configPath);
}
if (config.location) {
fs.readdir(config.location, (err) => {
if (err) {
fs.mkdir(config.location, { recursive: true }, (err) => {
if (err) {
console.log("Incorrect filepath in config");
downloadDir = homedir + "/ytDownloader";
} else {
downloadDir = config.location;
}
});
} else {
fs.writeFile(config.location + "/.test", "", (err) => {
// If that location is not accessible
if (err) {
downloadDir = homedir + "/ytDownloader";
console.log(
"Not allowed to use that location to save files"
);
} else {
downloadDir = config.location;
fs.rm(config.location + "/.test", (err) => {
if (err) console.log(err);
});
}
});
}
});
} else {
downloadDir = homedir + "/Videos/ytDownloader/";
fs.mkdirSync(homedir + "/Videos/ytDownloader/", { recursive: true });
}
});
//////
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static(__dirname + "/public"));
const htmlPath = __dirname + "/html/";
app.use(cookieParser());
// Clearing tempDir
fs.readdirSync(tempDir).forEach((f) => fs.rmSync(`${tempDir}/${f}`));
// Handling download location input from user
async function checkPath(path) {
const check = new Promise((resolve, reject) => {
fs.readdir(path, (err) => {
// If directory doesn't exist, try creating it
if (err) {
fs.mkdir(path, (err) => {
if (err) {
reject(err);
} else {
console.log("Successfully created " + path);
resolve(true);
}
});
} else {
fs.writeFile(path + "/.test", "", (err) => {
// If that location is not accessible
if (err) {
reject(err);
} else {
fs.rm(path + "/.test", (err) => {
if (err) console.log(err);
});
resolve(true);
}
});
}
});
});
const result = await check;
return result;
}
io.on("connection", (socket) => {
socket.emit("id", socket.id);
socket.on("downloadPath", () => {
socket.emit("downloadPath", downloadDir);
});
socket.on("newPath", (userPath) => {
let path;
if (userPath[userPath.length - 1] == "/") {
path = userPath;
} else {
path = userPath + "/";
}
checkPath(path)
.then((response) => {
const newConfig = {
location: path,
};
fs.writeFile(configPath, JSON.stringify(newConfig), (err) => {
if (!err) {
socket.emit("pathSaved", true);
downloadDir = path;
} else {
socket.emit("pathSaved", false);
}
});
})
.catch((error) => {
socket.emit("pathSaved", false);
});
});
});
// GET routes
app.get("/", (req, res) => {
res.sendFile(htmlPath + "index.html");
});
app.get("/about", (req, res) => {
res.sendFile(htmlPath + "about.html");
});
app.get("/preferences", (req, res) => {
res.sendFile(htmlPath + "preferences.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", async (req, res) => {
const socketId = req.cookies.id;
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 randomNum = Math.random().toFixed(5).toString("16").slice(2);
const filename = `${title}_${randomNum}_${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 extension = info.extension;
let filename = "";
// Trying to remove ambiguous characters
for (let i = 0; i < info.filename.length; i++) {
const pattern = /^[`~!@#$%^&*:;,<>?/|'"-+=\]\[]$/g;
let letter = "";
if (pattern.test(info.filename[i])) {
letter = "";
} else {
if (info.filename[i] == " ") {
letter = "_";
} else {
letter = info.filename[i];
}
}
filename += letter;
}
let audioExtension;
let videoProgress, audioProgress;
let audioTag;
if (extension == "mp4") {
audioTag = 140;
audioExtension = "m4a";
} else {
audioTag = 251;
audioExtension = "opus";
}
// If video
if (format.hasVideo) {
// Temporary audio and video files
let videoName =
Math.random().toString("16").slice(2) + "." + extension;
let audioName =
Math.random().toString("16").slice(2) + "." + audioExtension;
const arr = [
new Promise((resolve, reject) => {
// Downloading only video
ytdl(url, { quality: itag })
.on("progress", (_, downloaded, size) => {
videoProgress = (downloaded / size) * 100;
io.to(socketId).emit(
"videoProgress",
videoProgress
);
if (videoProgress == 100) {
resolve("video downloaded");
}
})
.pipe(fs.createWriteStream(tempDir + videoName));
}),
new Promise((resolve, reject) => {
// Downloading only audio
ytdl(url, {
highWaterMark: 1 << 25,
quality: audioTag,
})
.on("progress", (_, downloaded, size) => {
audioProgress = (downloaded / size) * 100;
io.to(socketId).emit(
"audioProgress",
audioProgress
);
if (audioProgress == 100) {
resolve("audio downloaded");
}
})
.pipe(fs.createWriteStream(tempDir + audioName));
}),
];
Promise.all([arr[0], arr[1]])
.then((response) => {
cp.exec(
`"${ffmpeg}" -i "${tempDir + videoName}" -i "${
tempDir + audioName
}" -c copy "${downloadDir + filename}"`,
(error, stdout, stderr) => {
if (error) {
console.log(error);
} else if (stderr) {
console.log("video saved");
// Clear temp dir
fs.readdirSync(tempDir).forEach((f) =>
fs.rmSync(`${tempDir}/${f}`)
);
io.to(socketId).emit("saved", `${downloadDir}`);
}
}
);
})
.catch((error) => {
console.log("Promise failed. " + error);
});
}
// If audio
else {
ytdl(url, { quality: itag })
.on("progress", (_, downloaded, size) => {
const progress = (downloaded / size) * 100;
if (progress == 100) {
io.to(socketId).emit("saved", `${downloadDir}`);
}
io.to(socketId).emit("onlyAudioProgress", progress);
})
.pipe(fs.createWriteStream(downloadDir + filename));
}
});
});
app.post("/test", (req, res) => {
console.log(req.body);
});
const PORT = 59876;
server.listen(PORT, () => {
console.log("Server: http://localhost:" + PORT);
});

@ -38,12 +38,24 @@ input[type="text"]{
top:15px;
right:15px;
font-size: large;
cursor: pointer;
}
a{
color:rgb(34, 136, 199);
cursor: pointer;
}
input[type="checkbox"]{
width:20px;
height:20px;
}
#selectLocation{
padding:10px;
border:none;
border-radius: 10px;
font-size: large;
color:white;
background-color: rgb(56, 209, 56);
cursor: pointer;
}

@ -52,6 +52,7 @@ body {
color:white;
text-decoration: none;
padding:5px;
cursor:pointer;
}
.menuItem:active, .menuItem:link{

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

@ -1,24 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>About</title>
<link rel="stylesheet" href="extra.css">
<link rel="stylesheet" href="../assets/css/extra.css">
</head>
<body>
<a href="/" id="back">Home</a>
<a id="back">Home</a>
<h1>ytDownloader</h1>
<p>ytDownloader allows you to download videos of all resolutions provided by YouTube.
They can be downloaded in different encoding formats. The same goes for audio files.
</p>
<p>It's a Free and Open Source app built on top of Node.js and Electron. ytdl-core has been used for downloading from YouTube</p>
<p>It's a Free and Open Source app built on top of Node.js and Electron. ytdl-core has been used for downloading
from YouTube</p>
<p>Source Code is available <a id="sourceLink">here</a></p>
<p>Source Code is available <a target="_blank" href="https://github.com/aandrew-me/ytDownloader">here</a></p>
<script>
const { ipcRenderer, shell } = require("electron")
document.getElementById("back").addEventListener("click", ()=>{
ipcRenderer.send("load-page", __dirname + "/index.html")
})
document.getElementById("sourceLink").addEventListener("click", ()=>{
shell.openExternal("https://github.com/aandrew-me/ytDownloader")
})
</script>
</body>
</html>

@ -6,8 +6,9 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Youtube Video Downloader</title>
<link rel="stylesheet" href="index.css">
<script src="index.js" defer></script>
<link rel="stylesheet" href="../assets/css/index.css">
<script src="../src/renderer.js" defer></script>
<script src="../src/index.js" defer></script>
</head>
@ -18,12 +19,12 @@
</div>
<!-- Menu icon -->
<img src="menu.png" alt="menu" id="menuIcon">
<img src="../assets/images/menu.png" alt="menu" id="menuIcon">
<!-- Menu -->
<div id="menu">
<a href="/preferences" class="menuItem">Preferences</a>
<a href="/about" class="menuItem">About</a>
<a id="preferenceWin" class="menuItem">Preferences</a>
<a id="aboutWin" class="menuItem">About</a>
</div>
@ -107,67 +108,6 @@
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
const electron = require("electron")
function getId(id) {
return (document.getElementById(id))
}
let totalProgress = 0
const socket = io();
socket.on("id", (id) => {
document.cookie = "id=" + id + "; SameSite=Strict"
})
// Video and audio progress
socket.on("videoProgress", (progress) => {
if (progress != 100) {
getId("videoProgressBox").style.display = "block"
getId("preparingBox").style.display = "none"
getId("videoProgress").value = progress
}
})
socket.on("audioProgress", (progress) => {
if (progress != 100) {
getId("preparingBox").style.display = "none"
getId("audioProgress").value = progress
}
})
////////////
// Only audio progress
socket.on("onlyAudioProgress", (progress) => {
if (progress != 100) {
getId("preparingBox").style.display = "none"
getId("audioProgressBox").style.display = "block"
getId("onlyAudioProgress").value = progress
}
})
socket.on("saved", (savedLocation) => {
const notify = new Notification('ytDownloader', {
body: "File saved successfully.",
icon: 'icon.png'
});
getId("videoProgressBox").style.display = "none";
getId("audioProgressBox").style.display = "none";
document.querySelector(".submitBtn").style.display = "inline-block"
getId("savedMsg").innerHTML = `File saved. Click to open <b title="Click to open" class="savedMsg">${savedLocation}</b>`
getId("savedMsg").addEventListener("click", (e) => {
electron.shell.openPath(savedLocation)
})
})
</script>
</body>
</html>

@ -5,46 +5,41 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Preferences</title>
<link rel="stylesheet" href="extra.css">
<link rel="stylesheet" href="../assets/css/extra.css">
</head>
<body>
<a href="/" id="back">Home</a>
<a id="back">Home</a>
<h1>Preferences</h1>
<p>1. Download location (Full path without quotation marks)</p>
<input type="text" id="savePath" placeholder="Full path without quotation marks">
<button id="save">Save</button>
<p>1. Download location</p>
<p>Default location: <span id="path"></span></p>
<button id="selectLocation">Select Download Location</button>
<p id="msg"></pid>
<p>2. Enable transparent dark (only Linux) <input type="checkbox" id="enableTransparent"></p>
<script src="/socket.io/socket.io.js"></script>
<script>
let downloadPath = localStorage.getItem("downloadPath")
getId("path").textContent = downloadPath
const { ipcRenderer, dialog } = require("electron")
function getId(id){
return document.getElementById(id)
}
let socket = io()
socket.emit("downloadPath")
socket.on("downloadPath", (message)=>{
const path = message
getId("savePath").value = message
getId("back").addEventListener("click", ()=>{
ipcRenderer.send("load-page", __dirname + "/index.html")
})
getId("save").addEventListener("click", ()=>{
const newPath = getId("savePath").value
socket.emit("newPath", newPath)
getId("selectLocation").addEventListener("click", ()=>{
ipcRenderer.send("select-location", "")
})
socket.on("pathSaved", (saved)=>{
if (saved){
getId("msg").textContent = "Location Saved successfully"
}
else{
getId("msg").textContent = "Not saved, some error has occured."
}
ipcRenderer.on("downloadPath", (event, downloadPath)=>{
console.log(downloadPath);
localStorage.setItem("downloadPath", downloadPath)
getId("path").textContent = downloadPath
})
const enabledTransparent = getId("enableTransparent")

@ -1,16 +1,14 @@
const { app, BrowserWindow, dialog } = require("electron");
const { app, BrowserWindow, dialog, ipcMain } = require("electron");
const { autoUpdater } = require("electron-updater");
const path = require("path");
require("./app.js");
let win
function createWindow() {
let isTransparent = false;
if (process.platform == "linux") {
isTransparent = true;
console.log("Using linux");
}
const win = new BrowserWindow({
win = new BrowserWindow({
show: false,
icon: __dirname + "/public/icon.png",
spellcheck: false,
@ -21,9 +19,11 @@ function createWindow() {
}
});
win.loadURL("http://localhost:59876");
win.loadFile("./html/index.html")
win.maximize();
win.setMenu(null)
win.show();
// win.webContents.openDevTools()
autoUpdater.checkForUpdatesAndNotify();
}
@ -40,6 +40,22 @@ app.whenReady().then(() => {
}
});
ipcMain.on("load-page", (event, arg) => {
win.loadFile(arg)
})
ipcMain.on("select-location", ()=>{
const location = dialog.showOpenDialogSync(win, {
properties: ['openFile', 'openDirectory']
})
if (location){
win.webContents.send("downloadPath", location)
}
})
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();

@ -1,18 +1,16 @@
{
"dependencies": {
"body-parser": "^1.20.0",
"cookie-parser": "^1.4.6",
"electron-updater": "^5.0.5",
"express": "^4.18.1",
"ffmpeg-static": "^5.0.2",
"socket.io": "^4.5.1",
"ytdl-core": "^4.11.0"
},
"name": "ytdownloader",
"version": "1.6.0",
"version": "1.7.0",
"main": "main.js",
"scripts": {
"start": "electron .",
"watch":"nodemon --exec electron .",
"debug":"electron --inspect=5858 .",
"windows": "rm -rf ./node_modules && electron-builder -w",
"linux": "rm -rf ./node_modules && electron-builder -l",
"mac": "rm -rf ./node_modules && electron-builder -m",

@ -1,306 +0,0 @@
const videoToggle = getId("videoToggle");
const audioToggle = getId("audioToggle");
const incorrectMsg = getId("incorrectMsg");
const loadingMsg = getId("loadingWrapper");
function getId(id) {
return document.getElementById(id);
}
function getInfo() {
incorrectMsg.textContent = "";
loadingMsg.style.display = "flex";
getId("videoFormatSelect").innerHTML = "";
getId("audioFormatSelect").innerHTML = "";
const url = getId("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";
getId("hidden").style.display = "inline-block";
getId("title").innerHTML = "<b>Title</b>: " + data.title;
getId("videoList").style.display = "block";
videoToggle.style.backgroundColor = "rgb(67, 212, 164)";
let highestQualityLength = 0;
let audioSize = 0;
// Getting approx size of audio file
for (let format of data.formats){
if (format.hasAudio && !format.hasVideo && format.contentLength && format.container == "mp4"){
audioSize = (Number(format.contentLength) / 1000000)
}
}
for (let format of data.formats) {
let size = (Number(format.contentLength) / 1000000).toFixed(2)
// For videos
if (
format.hasVideo &&
format.contentLength &&
!format.hasAudio
) {
size = (Number(size) + Number(audioSize)).toFixed(2)
size = size + " MB";
const itag = format.itag;
const avcPattern = /^avc1[0-9a-zA-Z.]+$/g;
const av1Pattern = /^av01[0-9a-zA-Z.]+$/g;
let codec;
if (av1Pattern.test(format.codecs)) {
codec = "AV1 Codec";
} else if (avcPattern.test(format.codecs)) {
codec = "AVC Codec";
} else {
codec = format.codecs.toUpperCase() + " Codec";
}
const element =
"<option value='" +
itag +
"'>" +
format.qualityLabel +
" | " +
format.container +
" | " +
size +
" | " +
codec;
("</option>");
getId("videoFormatSelect").innerHTML += element;
}
// For audios
else if (
format.hasAudio &&
!format.hasVideo &&
format.audioBitrate
) {
size = size + " MB";
const pattern = /^mp*4a[0-9.]+$/g;
let audioCodec;
const itag = format.itag;
if (pattern.test(format.audioCodec)) {
audioCodec = "m4a";
} else {
audioCodec = format.audioCodec;
}
const element =
"<option value='" +
itag +
"'>" +
format.audioBitrate +
" kbps" +
" | " +
audioCodec +
" | " +
size +
"</option>";
getId("audioFormatSelect").innerHTML += element;
}
}
} else {
loadingMsg.style.display = "none";
incorrectMsg.textContent =
"Some error has occured. Check your connection and use correct URL";
}
})
.catch((error) => {
if (error) {
loadingMsg.style.display = "none";
incorrectMsg.textContent = error;
}
});
}
function download(type) {
getId("videoProgressBox").style.display = "none";
getId("audioProgressBox").style.display = "none";
getId("savedMsg").innerHTML = "";
const url = getId("url").value;
let itag;
let options;
if (type === "video") {
itag = getId("videoFormatSelect").value;
options = {
method: "POST",
body: new URLSearchParams({
url: url,
videoTag: itag,
}),
headers: {
"Content-Type": "Application/x-www-form-urlencoded",
},
};
} else {
itag = getId("audioFormatSelect").value;
options = {
method: "POST",
body: new URLSearchParams({
url: url,
audioTag: itag,
}),
headers: {
"Content-Type": "Application/x-www-form-urlencoded",
},
};
}
fetch("/download", options)
.then((response) => response.json)
.then((data) => {
console.log(data);
})
.catch((error) => {
console.log(error);
});
}
let menuIsOpen = false;
getId("menuIcon").addEventListener("click", (event) => {
if (menuIsOpen) {
getId("menuIcon").style.transform = "rotate(0deg)";
menuIsOpen = false;
let count = 0;
let opacity = 1
const fade = setInterval(() => {
if (count >= 10) {
clearInterval(fade);
} else {
opacity -= .1
getId("menu").style.opacity = opacity;
count++;
}
}, 50);
} else {
getId("menuIcon").style.transform = "rotate(90deg)";
menuIsOpen = true;
setTimeout(() => {
getId("menu").style.display = "flex";
getId("menu").style.opacity = 1;
}, 150);
}
});
getId("videoDownload").addEventListener("click", (event) => {
getId("preparingBox").style.display = "flex";
clickAnimation("videoDownload");
download("video");
});
getId("audioDownload").addEventListener("click", (event) => {
getId("preparingBox").style.display = "flex";
clickAnimation("audioDownload");
download("audio");
});
// Getting video info
getId("getInfo").addEventListener("click", (event) => {
getInfo();
});
getId("url").addEventListener("keypress", (event) => {
if (event.key == "Enter") {
getInfo();
}
});
// Video and audio toggle
videoToggle.addEventListener("click", (event) => {
videoToggle.style.backgroundColor = "var(--box-toggleOn)";
audioToggle.style.backgroundColor = "var(--box-toggle)";
getId("audioList").style.display = "none";
getId("videoList").style.display = "block";
});
audioToggle.addEventListener("click", (event) => {
audioToggle.style.backgroundColor = "var(--box-toggleOn)";
videoToggle.style.backgroundColor = "var(--box-toggle)";
getId("videoList").style.display = "none";
getId("audioList").style.display = "block";
});
/////////////
// Toggle theme
let darkTheme = false;
let circle = getId("themeToggleInside");
const root = document.querySelector(":root");
let enabledTransparent = localStorage.getItem("enabledTransparent")
let bgColor = ""
if (enabledTransparent == "true"){
bgColor = "rgba(40,40,40, .7)"
}
else{
bgColor = "rgb(40,40,40)"
}
function toggle() {
if (darkTheme == false) {
circle.style.left = "25px";
root.style.setProperty("--background", bgColor);
root.style.setProperty("--text", "white");
root.style.setProperty("--box-main", "rgb(80,80,80)");
root.style.setProperty("--box-toggle", "rgb(70,70,70)");
root.style.setProperty("--theme-toggle", "rgb(80, 193, 238)");
darkTheme = true;
localStorage.setItem("theme", "dark");
} else {
circle.style.left = "0px";
root.style.setProperty("--background", "whitesmoke");
root.style.setProperty("--text", "rgba(45, 45, 45)");
root.style.setProperty("--box-main", "rgb(143, 239, 207)");
root.style.setProperty("--box-toggle", "rgb(108, 231, 190)");
root.style.setProperty("--theme-toggle", "rgb(147, 174, 185)");
darkTheme = false;
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) {
getId(id).style.animationName = "clickAnimation";
setTimeout(() => {
getId(id).style.animationName = "";
}, 500);
}

@ -0,0 +1,116 @@
const videoToggle = getId("videoToggle");
const audioToggle = getId("audioToggle");
const incorrectMsg = getId("incorrectMsg");
const loadingMsg = getId("loadingWrapper");
function getId(id) {
return document.getElementById(id);
}
let menuIsOpen = false;
getId("menuIcon").addEventListener("click", (event) => {
if (menuIsOpen) {
getId("menuIcon").style.transform = "rotate(0deg)";
menuIsOpen = false;
let count = 0;
let opacity = 1
const fade = setInterval(() => {
if (count >= 10) {
clearInterval(fade);
} else {
opacity -= .1
getId("menu").style.opacity = opacity;
count++;
}
}, 50);
} else {
getId("menuIcon").style.transform = "rotate(90deg)";
menuIsOpen = true;
setTimeout(() => {
getId("menu").style.display = "flex";
getId("menu").style.opacity = 1;
}, 150);
}
});
// Video and audio toggle
videoToggle.addEventListener("click", (event) => {
videoToggle.style.backgroundColor = "var(--box-toggleOn)";
audioToggle.style.backgroundColor = "var(--box-toggle)";
getId("audioList").style.display = "none";
getId("videoList").style.display = "block";
});
audioToggle.addEventListener("click", (event) => {
audioToggle.style.backgroundColor = "var(--box-toggleOn)";
videoToggle.style.backgroundColor = "var(--box-toggle)";
getId("videoList").style.display = "none";
getId("audioList").style.display = "block";
});
/////////////
// Toggle theme
let darkTheme = false;
let circle = getId("themeToggleInside");
const root = document.querySelector(":root");
let enabledTransparent = localStorage.getItem("enabledTransparent")
let bgColor = ""
if (enabledTransparent == "true"){
bgColor = "rgba(40,40,40, .7)"
}
else{
bgColor = "rgb(40,40,40)"
}
function toggle() {
if (darkTheme == false) {
circle.style.left = "25px";
root.style.setProperty("--background", bgColor);
root.style.setProperty("--text", "white");
root.style.setProperty("--box-main", "rgb(80,80,80)");
root.style.setProperty("--box-toggle", "rgb(70,70,70)");
root.style.setProperty("--theme-toggle", "rgb(80, 193, 238)");
darkTheme = true;
localStorage.setItem("theme", "dark");
} else {
circle.style.left = "0px";
root.style.setProperty("--background", "whitesmoke");
root.style.setProperty("--text", "rgba(45, 45, 45)");
root.style.setProperty("--box-main", "rgb(143, 239, 207)");
root.style.setProperty("--box-toggle", "rgb(108, 231, 190)");
root.style.setProperty("--theme-toggle", "rgb(147, 174, 185)");
darkTheme = false;
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) {
getId(id).style.animationName = "clickAnimation";
setTimeout(() => {
getId(id).style.animationName = "";
}, 500);
}

@ -0,0 +1,423 @@
const fs = require("fs");
const ytdl = require("ytdl-core");
const cp = require("child_process");
const os = require("os");
const ffmpeg = require("ffmpeg-static");
const { BrowserWindow, shell, remote, ipcRenderer } = require("electron");
// Directories
const homedir = os.homedir();
const appdir = homedir + "/.ytDownloader/";
const tempDir = appdir + "temp/";
fs.mkdirSync(homedir + "/.ytDownloader/temp", { recursive: true });
let config;
// Download directory
let downloadDir = "";
function getId(id) {
return document.getElementById(id);
}
let localPath = localStorage.getItem("downloadPath")
if (localPath){
downloadDir = localPath
}
else{
downloadDir = appdir
}
// Clearing tempDir
fs.readdirSync(tempDir).forEach((f) => fs.rmSync(`${tempDir}/${f}`));
// Handling download location input from user
async function checkPath(path) {
const check = new Promise((resolve, reject) => {
fs.readdir(path, (err) => {
// If directory doesn't exist, try creating it
if (err) {
fs.mkdir(path, (err) => {
if (err) {
reject(err);
} else {
console.log("Successfully created " + path);
resolve(true);
}
});
} else {
fs.writeFile(path + "/.test", "", (err) => {
// If that location is not accessible
if (err) {
reject(err);
} else {
fs.rm(path + "/.test", (err) => {
if (err) console.log(err);
});
resolve(true);
}
});
}
});
});
const result = await check;
return result;
}
// Collecting info from youtube
async function getVideoInfo(url) {
let info;
await ytdl.getInfo(url)
.then((data)=>{
info = data
})
.catch((error)=>{
})
return info;
}
// Getting video info
async function getInfo() {
incorrectMsg.textContent = "";
loadingMsg.style.display = "flex";
getId("videoFormatSelect").innerHTML = "";
getId("audioFormatSelect").innerHTML = "";
const url = getId("url").value;
let info = await getVideoInfo(url)
if (info) {
let title = info.videoDetails.title;
let formats = info.formats;
console.log(formats);
const urlElements = document.querySelectorAll(".url");
urlElements.forEach((element) => {
element.value = url;
});
loadingMsg.style.display = "none";
getId("hidden").style.display = "inline-block";
getId("title").innerHTML = "<b>Title</b>: " + title;
getId("videoList").style.display = "block";
videoToggle.style.backgroundColor = "rgb(67, 212, 164)";
let highestQualityLength = 0;
let audioSize = 0;
// Getting approx size of audio file
for (let format of formats) {
if (
format.hasAudio &&
!format.hasVideo &&
format.contentLength &&
format.container == "mp4"
) {
audioSize = Number(format.contentLength) / 1000000;
}
}
for (let format of formats) {
let size = (Number(format.contentLength) / 1000000).toFixed(2);
// For videos
if (format.hasVideo && format.contentLength && !format.hasAudio) {
size = (Number(size) + Number(audioSize)).toFixed(2);
size = size + " MB";
const itag = format.itag;
const avcPattern = /^avc1[0-9a-zA-Z.]+$/g;
const av1Pattern = /^av01[0-9a-zA-Z.]+$/g;
let codec;
if (av1Pattern.test(format.codecs)) {
codec = "AV1 Codec";
} else if (avcPattern.test(format.codecs)) {
codec = "AVC Codec";
} else {
codec = format.codecs.toUpperCase() + " Codec";
}
const element =
"<option value='" +
itag +
"'>" +
format.qualityLabel +
" | " +
format.container +
" | " +
size +
" | " +
codec;
("</option>");
getId("videoFormatSelect").innerHTML += element;
}
// For audios
else if (
format.hasAudio &&
!format.hasVideo &&
format.audioBitrate
) {
size = size + " MB";
const pattern = /^mp*4a[0-9.]+$/g;
let audioCodec;
const itag = format.itag;
if (pattern.test(format.audioCodec)) {
audioCodec = "m4a";
} else {
audioCodec = format.audioCodec;
}
const element =
"<option value='" +
itag +
"'>" +
format.audioBitrate +
" kbps" +
" | " +
audioCodec +
" | " +
size +
"</option>";
getId("audioFormatSelect").innerHTML += element;
}
}
} else {
loadingMsg.style.display = "none";
incorrectMsg.textContent =
"Some error has occured. Check your connection and use correct URL";
}
}
getId("getInfo").addEventListener("click", (event) => {
getInfo();
});
getId("url").addEventListener("keypress", (event) => {
if (event.key == "Enter") {
getInfo();
}
});
// Video download event
getId("videoDownload").addEventListener("click", (event) => {
getId("preparingBox").style.display = "flex";
clickAnimation("videoDownload");
download("video");
});
// Audio download event
getId("audioDownload").addEventListener("click", (event) => {
getId("preparingBox").style.display = "flex";
clickAnimation("audioDownload");
download("audio");
});
// Downloading with ytdl
function download(type) {
getId("videoProgressBox").style.display = "none";
getId("audioProgressBox").style.display = "none";
getId("savedMsg").innerHTML = "";
const url = getId("url").value;
let itag;
if (type === "video") {
itag = getId("videoFormatSelect").value;
} else {
itag = getId("audioFormatSelect").value;
}
// Finding info of the link and downloading
findInfo(url, itag).then((info) => {
const format = info.format;
const extension = info.extension;
let filename = "";
// Trying to remove ambiguous characters
for (let i = 0; i < info.filename.length; i++) {
const pattern = /^[`~!@#$%^&*:;,<>?/|'"-+=\]\[]$/g;
let letter = "";
if (pattern.test(info.filename[i])) {
letter = "";
} else {
if (info.filename[i] == " ") {
letter = "_";
} else {
letter = info.filename[i];
}
}
filename += letter;
}
let audioExtension;
let videoProgress, audioProgress;
let audioTag;
if (extension == "mp4") {
audioTag = 140;
audioExtension = "m4a";
} else {
audioTag = 251;
audioExtension = "opus";
}
// If video
if (format.hasVideo) {
// Temporary audio and video files
let videoName =
Math.random().toString("16").slice(2) + "." + extension;
let audioName =
Math.random().toString("16").slice(2) + "." + audioExtension;
const arr = [
new Promise((resolve, reject) => {
// Downloading only video
ytdl(url, { quality: itag })
.on("progress", (_, downloaded, size) => {
videoProgress = (downloaded / size) * 100;
if (videoProgress != 100) {
getId("videoProgressBox").style.display =
"block";
getId("preparingBox").style.display = "none";
getId("videoProgress").value = progress;
}
if (videoProgress == 100) {
resolve("video downloaded");
}
})
.pipe(fs.createWriteStream(tempDir + videoName));
}),
new Promise((resolve, reject) => {
// Downloading only audio
ytdl(url, {
highWaterMark: 1 << 25,
quality: audioTag,
})
.on("progress", (_, downloaded, size) => {
audioProgress = (downloaded / size) * 100;
if (audioProgress != 100) {
getId("preparingBox").style.display = "none";
getId("audioProgress").value = progress;
} else if (audioProgress == 100) {
resolve("audio downloaded");
}
})
.pipe(fs.createWriteStream(tempDir + audioName));
}),
];
Promise.all([arr[0], arr[1]])
.then((response) => {
cp.exec(
`"${ffmpeg}" -i "${tempDir + videoName}" -i "${
tempDir + audioName
}" -c copy "${downloadDir + "/" + filename}"`,
(error, stdout, stderr) => {
if (error) {
console.log(error);
} else if (stderr) {
console.log("video saved");
// Clear temp dir
fs.readdirSync(tempDir).forEach((f) =>
fs.rmSync(`${tempDir}/${f}`)
);
afterSave(downloadDir, filename);
}
}
);
})
.catch((error) => {
console.log("Promise failed. " + error);
});
}
// If audio
else {
ytdl(url, { quality: itag })
.on("progress", (_, downloaded, size) => {
const progress = (downloaded / size) * 100;
if (progress != 100) {
getId("preparingBox").style.display = "none";
getId("audioProgressBox").style.display = "block";
getId("onlyAudioProgress").value = progress;
} else if (progress == 100) {
afterSave(downloadDir, filename);
}
})
.pipe(fs.createWriteStream(downloadDir + "/" + filename));
}
});
}
// After saving video
function afterSave(location, filename) {
const notify = new Notification("ytDownloader", {
body: "File saved successfully.",
icon: "../assets/images/icon.png",
});
getId("videoProgressBox").style.display = "none";
getId("audioProgressBox").style.display = "none";
document.querySelector(".submitBtn").style.display = "inline-block";
getId(
"savedMsg"
).innerHTML = `File saved. Click to open <b title="Click to open" class="savedMsg">${location}</b>`;
getId("savedMsg").addEventListener("click", (e) => {
shell.showItemInFolder(location + "/" + filename);
});
}
// 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 randomNum = Math.random().toFixed(5).toString("16").slice(2);
const filename = `${title}_${randomNum}_${quality}.${extension}`;
const info = {
format: format,
title: title,
extension: extension,
quality: quality,
filename: filename,
};
return info;
}
// Opening windows
getId("preferenceWin").addEventListener("click", () => {
ipcRenderer.send("load-page", __dirname + "/preferences.html")
});
getId("aboutWin").addEventListener("click", ()=>{
ipcRenderer.send("load-page", __dirname + "/about.html")
})
Loading…
Cancel
Save