Major backend changes and some UX improvements
parent
6040260c0c
commit
aca0efeabe
@ -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);
|
||||
});
|
||||
|
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>
|
||||
@ -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…
Reference in New Issue