Added support for custom download location

Optimization improvements and bug fixes
pull/9/head
aandrew-me 3 years ago
parent 1c7907d80a
commit 071855f9af

162
app.js

@ -13,22 +13,158 @@ const ffmpeg = require("ffmpeg-static");
const cp = require("child_process");
const os = require("os");
// Directories
const homedir = os.homedir();
fs.mkdirSync(homedir + "/Videos/ytDownloader/temp", { recursive: true });
const downloadDir = homedir + "/Videos/ytDownloader/";
const tempDir = homedir + "/Videos/ytDownloader/temp/";
fs.readdirSync(tempDir).forEach((f) => fs.rmSync(`${tempDir}/${f}`));
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 + "/ytDownloader/";
}
});
//////
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(__dirname + "/index.html");
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) {
@ -182,9 +318,6 @@ app.post("/download", async (req, res) => {
);
io.to(socketId).emit("saved", `${downloadDir}`);
}
if (stdout) {
console.log("stdout this time");
}
}
);
})
@ -198,12 +331,11 @@ app.post("/download", async (req, res) => {
ytdl(url, { quality: itag })
.on("progress", (_, downloaded, size) => {
const progress = (downloaded / size) * 100;
if (progress == 100){
if (progress == 100) {
io.to(socketId).emit("saved", `${downloadDir}`);
}
io.sockets
.to(req.cookies.id)
.emit("audioProgress", progress);
io.to(socketId)
.emit("onlyAudioProgress", progress);
})
.pipe(fs.createWriteStream(downloadDir + filename));
}
@ -214,6 +346,8 @@ app.post("/test", (req, res) => {
console.log(req.body);
});
server.listen(3000, () => {
console.log("Server: http://localhost:3000");
const PORT=59876
server.listen(PORT , () => {
console.log("Server: http://localhost:" + PORT);
});

@ -0,0 +1,24 @@
<!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">
</head>
<body>
<a href="/" 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>Source Code is available <a target="_blank" href="https://github.com/aandrew-me/ytDownloader">here</a></p>
</body>
</html>

@ -17,6 +17,16 @@
<div id="themeToggleInside"></div>
</div>
<!-- Menu icon -->
<img src="menu.png" alt="menu" id="menuIcon">
<!-- Menu -->
<div id="menu">
<a href="/preferences" class="menuItem">Preferences</a>
<a href="/about" class="menuItem">About</a>
</div>
<h1>YouTube Downloader</h1>
<input type="text" name="url" placeholder="Paste Video URL or ID here" id="url" autofocus>
<!-- Get info button -->
@ -84,17 +94,25 @@
<p id="savedMsg"></p>
<div id="progressBox">
<div class="progressBox" id="videoProgressBox">
<label>Video Progress: <progress max="100" value="0" id="videoProgress"></progress></label>
<br>
<label>Audio Progress: <progress max="100" value="0" id="audioProgress"></progress></label>
</div>
<div class="progressBox" id="audioProgressBox">
<label>Download Progress: <progress max="100" value="0" id="onlyAudioProgress"></progress></label>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
function getId(id) {
return (document.getElementById(id))
}
let totalProgress = 0
const socket = io();
@ -102,32 +120,45 @@
document.cookie = "id=" + id + "; SameSite=Strict"
})
// Video and audio progress
socket.on("videoProgress", (progress) => {
document.getElementById("progressBox").style.display = "inline-block"
document.getElementById("preparingBox").style.display = "none"
document.getElementById("videoProgress").value = progress
if (progress != 100) {
document.querySelector(".submitBtn").style.display = "none"
}
else {
document.querySelector(".submitBtn").style.display = "inline-block"
getId("videoProgressBox").style.display = "block"
getId("preparingBox").style.display = "none"
getId("videoProgress").value = progress
}
})
socket.on("audioProgress", (progress) => {
document.getElementById("progressBox").style.display = "inline-block"
document.getElementById("preparingBox").style.display = "none"
document.getElementById("audioProgress").value = 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('ytDownloaded', {
body: "Video saved successfully.",
body: "File saved successfully.",
icon: 'icon.png'
});
document.getElementById("savedMsg").innerHTML = `Video saved to <a href='file://${savedLocation}'>${savedLocation}</a>`
document.getElementById("progressBox").style.display = "none"
getId("videoProgressBox").style.display = "none";
getId("audioProgressBox").style.display = "none";
document.querySelector(".submitBtn").style.display = "inline-block"
getId("savedMsg").innerHTML = `File saved to <a class="savedMsg" href='file://${savedLocation}'>${savedLocation}</a>`
})
</script>
</body>

@ -0,0 +1,49 @@
<!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>Preferences</title>
<link rel="stylesheet" href="extra.css">
</head>
<body>
<a href="/" 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 id="msg"></pid>
<script src="/socket.io/socket.io.js"></script>
<script>
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("save").addEventListener("click", ()=>{
const newPath = getId("savePath").value
socket.emit("newPath", newPath)
})
socket.on("pathSaved", (saved)=>{
if (saved){
getId("msg").textContent = "Location Saved successfully"
}
else{
getId("msg").textContent = "Not saved, some error has occured."
}
})
</script>
</body>
</html>

@ -1,4 +1,5 @@
const { app, BrowserWindow } = require('electron')
const { autoUpdater } = require("electron-updater")
const path = require('path')
require("./app.js")
@ -7,14 +8,14 @@ function createWindow () {
show:false
})
win.loadURL("http://localhost:3000")
win.loadURL("http://localhost:59876")
win.maximize()
win.show()
}
app.whenReady().then(() => {
createWindow()
autoUpdater.checkForUpdatesAndNotify()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()

@ -2,21 +2,22 @@
"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.4.4",
"version": "1.5.4",
"main": "main.js",
"scripts": {
"start": "electron .",
"windows": "rm -rf ./node_modules && electron-builder -w",
"linux": "rm -rf ./node_modules && electron-builder -l",
"mac": "rm -rf ./node_modules && electron-builder -m",
"publish-linux":"rm -rf ./node_modules && electron-builder -l --publish=always",
"publish-windows":"rm -rf ./node_modules && electron-builder -w --publish=always"
"publish-linux": "rm -rf ./node_modules && electron-builder -l --publish=always",
"publish-windows": "rm -rf ./node_modules && electron-builder -w --publish=always"
},
"author": {
"name": "Andrew",
@ -59,7 +60,8 @@
},
"linux": {
"target": [
"AppImage"
"AppImage",
"deb"
],
"category": "Utility"
},

@ -0,0 +1,44 @@
body{
background-color: rgb(50,50,50);
color:whitesmoke;
padding:20px;
font-size: x-large;
text-align: left;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
h1{
margin-top:0;
}
input[type="text"]{
padding:10px;
border-radius: 5px;
outline: none;
border:none;
width:60%;
}
#save{
padding:10px;
border-radius: 8px;
border:none;
color:rgb(255, 255, 255);
background-color: rgb(56, 209, 56);
cursor: pointer;
}
#save:active{
background-color: rgb(41, 155, 41);
}
#back{
text-decoration: none;
padding:8px;
border-radius: 8px;
background-color: rgb(51, 177, 219);
color:white;
position: absolute;
top:15px;
right:15px;
font-size: large;
}
a{
color:rgb(34, 136, 199);
}

@ -1,10 +1,10 @@
:root{
:root {
--background: whitesmoke;
--text:rgb(20, 20, 20);
--box-main:rgb(143, 239, 207);
--box-toggle:rgb(108, 231, 190);
--text: rgb(20, 20, 20);
--box-main: rgb(143, 239, 207);
--box-toggle: rgb(108, 231, 190);
--box-toggleOn: rgb(67, 212, 164);
--theme-toggle:rgb(147, 174, 185);
--theme-toggle: rgb(147, 174, 185);
}
body {
@ -15,7 +15,7 @@ body {
font-size: xx-large;
padding: 10px;
background-color: var(--background);
color:var(--text)
color: var(--text);
}
#title {
@ -23,6 +23,42 @@ body {
font-family: sans-serif;
}
#menuIcon{
position: absolute;
top:10px;
right:10px;
width:40px;
height:40px;
cursor: pointer;
transition: .3s;
}
#menu{
display:none;
flex-direction: column;
backdrop-filter: blur(16px) saturate(160%);
-webkit-backdrop-filter: blur(16px) saturate(160%);
background-color: rgba(17, 25, 40, 0.75);
border-radius: 12px;
position: absolute;
top:40px;
right:50px;
padding:15px;
text-align: left;
opacity:1;
font-size: large;
}
.menuItem{
color:white;
text-decoration: none;
padding:5px;
}
.menuItem:active, .menuItem:link{
color:white;
}
h1 {
font-size: 50px;
}
@ -46,14 +82,20 @@ input[type="text"] {
cursor: pointer;
/* animation-name: clickAnimation; */
animation-duration: .5s;
animation-duration: 0.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);}
0% {
background-color: rgb(64, 227, 64);
}
50% {
background-color: rgb(40, 126, 40);
}
100% {
background-color: rgb(64, 227, 64);
}
}
#hidden {
@ -63,10 +105,10 @@ input[type="text"] {
border-radius: 10px;
width: 80%;
padding: 10px 10px 20px 10px;
color:var(--text);
color: var(--text);
}
#progressBox{
display:none;
#videoProgressBox, #audioProgressBox {
display: none;
}
#btnContainer {
@ -83,7 +125,7 @@ input[type="text"] {
border-radius: 10px;
cursor: pointer;
padding: 8px;
color:var(--text)
color: var(--text);
}
select {
@ -94,7 +136,7 @@ select {
cursor: pointer;
font-size: large;
margin: 8px;
outline:none;
outline: none;
}
label {
@ -116,9 +158,9 @@ label {
border: none;
font-size: large;
cursor: pointer;
display:inline-block;
animation-duration: .5s;
display: inline-block;
animation-duration: 0.5s;
animation-timing-function: linear;
}
@ -126,15 +168,15 @@ label {
color: rgb(250, 59, 59);
}
#loadingWrapper{
display:none;
#loadingWrapper {
display: none;
flex-direction: row;
justify-content: center;
align-items: center;
}
#preparingBox{
display:none;
#preparingBox {
display: none;
flex-direction: row;
justify-content: center;
align-items: center;
@ -144,8 +186,8 @@ svg {
width: 100px;
height: 100px;
display: inline-block;
margin-left:20px
}
margin-left: 20px;
}
#themeToggle {
width: 55px;
@ -167,25 +209,28 @@ svg {
position: relative;
transition: linear;
transition-duration: 0.4s;
left:0px;
left: 0px;
}
a{
color:rgb(131, 222, 253);
.savedMsg {
color: rgb(131, 222, 253);
}
a:active, a:link{
color:rgb(131, 222, 253)
.savedMsg:active,
.savedMsg:link {
color: rgb(131, 222, 253);
}
/* Scrollbar */
body::-webkit-scrollbar {
width: 5px;
}
body::-webkit-scrollbar-track {
}
body::-webkit-scrollbar-track {
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
}
body::-webkit-scrollbar-thumb {
}
body::-webkit-scrollbar-thumb {
background-color: rgb(79, 78, 78);
border-radius: 5px;
}
}

@ -1,14 +1,18 @@
const videoToggle = document.getElementById("videoToggle");
const audioToggle = document.getElementById("audioToggle");
const incorrectMsg = document.getElementById("incorrectMsg");
const loadingMsg = document.getElementById("loadingWrapper");
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";
document.getElementById("videoFormatSelect").innerHTML = "";
document.getElementById("audioFormatSelect").innerHTML = "";
const url = document.getElementById("url").value;
getId("videoFormatSelect").innerHTML = "";
getId("audioFormatSelect").innerHTML = "";
const url = getId("url").value;
const options = {
method: "POST",
body: "url=" + url,
@ -30,27 +34,32 @@ function getInfo() {
if (data.status == true) {
loadingMsg.style.display = "none";
document.getElementById("hidden").style.display =
"inline-block";
document.getElementById("title").innerHTML =
"<b>Title</b>: " + data.title;
document.getElementById("videoList").style.display = "block";
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;
for (let format of data.formats) {
let size = (Number(format.contentLength) / 1000000).toFixed(
2
);
size = size + " MB";
// 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;
@ -76,9 +85,7 @@ function getInfo() {
" | " +
codec;
("</option>");
document.getElementById(
"videoFormatSelect"
).innerHTML += element;
getId("videoFormatSelect").innerHTML += element;
}
// For audios
@ -87,12 +94,13 @@ function getInfo() {
!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 = "mp4a";
audioCodec = "m4a";
} else {
audioCodec = format.audioCodec;
}
@ -107,15 +115,13 @@ function getInfo() {
" | " +
size +
"</option>";
document.getElementById(
"audioFormatSelect"
).innerHTML += element;
getId("audioFormatSelect").innerHTML += element;
}
}
} else {
loadingMsg.style.display = "none";
incorrectMsg.textContent =
"Some error has occured. Check your connection or the URL";
"Some error has occured. Check your connection and use correct URL";
}
})
.catch((error) => {
@ -127,13 +133,15 @@ function getInfo() {
}
function download(type) {
document.getElementById("progressBox").style.display = "none"
document.getElementById("savedMsg").innerHTML = ""
const url = document.getElementById("url").value;
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 = document.getElementById("videoFormatSelect").value;
itag = getId("videoFormatSelect").value;
options = {
method: "POST",
body: new URLSearchParams({
@ -145,7 +153,7 @@ function download(type) {
},
};
} else {
itag = document.getElementById("audioFormatSelect").value;
itag = getId("audioFormatSelect").value;
options = {
method: "POST",
body: new URLSearchParams({
@ -167,45 +175,82 @@ function download(type) {
});
}
document.getElementById("videoDownload").addEventListener("click", (event) => {
document.getElementById("preparingBox").style.display = "flex";
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);
console.log("end")
} else {
opacity -= .1
getId("menu").style.opacity = opacity;
console.log("doing")
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");
});
document.getElementById("audioDownload").addEventListener("click", (event) => {
document.getElementById("preparingBox").style.display = "flex";
getId("audioDownload").addEventListener("click", (event) => {
getId("preparingBox").style.display = "flex";
clickAnimation("audioDownload");
download("audio");
});
document.getElementById("getInfo").addEventListener("click", (event) => {
// Getting video info
getId("getInfo").addEventListener("click", (event) => {
getInfo();
});
document.getElementById("url").addEventListener("keypress", (event) => {
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)";
document.getElementById("audioList").style.display = "none";
document.getElementById("videoList").style.display = "block";
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)";
document.getElementById("videoList").style.display = "none";
document.getElementById("audioList").style.display = "block";
getId("videoList").style.display = "none";
getId("audioList").style.display = "block";
});
/////////////
// Toggle theme
let darkTheme = false;
let circle = document.getElementById("themeToggleInside");
let circle = getId("themeToggleInside");
const root = document.querySelector(":root");
function toggle() {
@ -246,9 +291,9 @@ if (storageTheme == "dark") {
////
function clickAnimation(id) {
document.getElementById(id).style.animationName = "clickAnimation";
getId(id).style.animationName = "clickAnimation";
setTimeout(() => {
document.getElementById(id).style.animationName = "";
getId(id).style.animationName = "";
}, 500);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Loading…
Cancel
Save