Initial version

pull/9/head
aandrew-me 3 years ago
commit 80411aeb08

@ -0,0 +1,2 @@
[Desktop Entry]
Icon=/home/andrew/Pictures/icons/icon.png

4
.gitignore vendored

@ -0,0 +1,4 @@
node_modules
release
package-lock.json
test.js

200
app.js

@ -0,0 +1,200 @@
const fs = require("fs");
const ytdl = require("ytdl-core");
const express = require("express");
const app = express();
const bodyParser = require("body-parser");
const ffmpeg = require("ffmpeg-static");
const os = require("os");
const cp = require("child_process");
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static(__dirname + "/public"));
// To do
// Preset
// Download location selection
// Choosing correct encoding
app.get("/", (req, res) => {
res.sendFile(__dirname + "/index.html");
});
async function getVideoInfo(url) {
const info = await ytdl.getInfo(url);
return info;
}
app.post("/", (req, res) => {
const url = req.body.url;
getVideoInfo(url)
.then((video) =>
res.json({
status: true,
title: video.videoDetails.title,
formats: video.formats,
url: url,
})
)
.catch((error) =>
res.json({ status: false, message: "Use correct URL" })
);
});
// Downloading video
app.post("/download", (req, res) => {
const itag = parseInt(req.body.audioTag || req.body.videoTag);
const url = req.body.url;
// Function to find video/audio info
async function findInfo(url, itag) {
const data = await ytdl.getInfo(url);
const format = ytdl.chooseFormat(data.formats, { quality: itag });
const title = data.videoDetails.title;
let extension;
if (format.hasVideo) {
extension = format.mimeType.split("; ")[0].split("/")[1];
} else {
if (format.audioCodec === "mp4a.40.2") {
extension = "m4a";
} else {
extension = format.audioCodec;
}
}
let quality;
if (format.hasVideo) {
quality = format.qualityLabel;
} else {
quality = format.audioBitrate + "kbps";
}
const filename = title + "_" + quality + "." + extension
const info = {
format: format,
title: title,
extension: extension,
quality:quality,
filename:filename
};
return info;
}
findInfo(url, itag).then((info) => {
const format = info.format;
const filename = info.filename;
// If video
if (format.hasVideo) {
let video = ytdl(url, { quality: itag });
let audio = ytdl(url, {
filter: "audioonly",
highWaterMark: 1 << 25,
});
const ffmpegProcess = cp.spawn(
ffmpeg,
[
"-i",
`pipe:3`,
"-i",
`pipe:4`,
"-map",
"0:v",
"-map",
"1:a",
"-c:v",
"copy",
"-c:a",
"libmp3lame",
"-crf",
"27",
"-preset",
"fast",
"-movflags",
"frag_keyframe+empty_moov",
'-f', "mp4",
"-loglevel",
"error",
"-",
],
{
stdio: ["pipe", "pipe", "pipe", "pipe", "pipe"],
}
);
video.pipe(ffmpegProcess.stdio[3]);
audio.pipe(ffmpegProcess.stdio[4]);
res.header("Content-Disposition", "attachment; filename='" + filename + "'");
ffmpegProcess.stdio[1].pipe(res);
}
// If audio
else {
res.header("Content-Disposition", "attachment; filename=" + filename);
ytdl(url, { quality: itag })
.on("progress", (_, downloaded, size) => {
const progress = (downloaded / size) * 100;
})
// .pipe(fs.createWriteStream(os.homedir() + "/Downloads/" + filename))
.pipe(res);
}
});
});
// Off for now
app.post("", (req, res) => {
const itag = parseInt(req.body.audioTag || req.body.videoTag);
const url = req.body.url;
async function findFormat(url, itag) {
const info = await ytdl.getInfo(url);
const format = ytdl.chooseFormat(info.formats, { quality: itag });
const title = info.videoDetails.title;
// Quality for filename
let quality;
if (format.hasVideo) {
quality = format.qualityLabel;
} else {
quality = format.audioBitrate + "kbps";
}
// File extension for filename
let extension;
if (format.hasVideo) {
extension = format.mimeType.split("; ")[0].split("/")[1];
} else {
if (format.audioCodec === "mp4a.40.2") {
extension = "m4a";
} else {
extension = format.audioCodec;
}
}
const filename = title + "_" + quality + "." + extension;
return filename;
}
findFormat(url, itag)
.then((filename) => {
res.header("Content-Disposition", "attachment; filename=.m4a");
ytdl(url, { quality: itag })
.on("progress", (_, downloaded, size) => {
const progress = (downloaded / size) * 100;
// console.log("Progress: " + progress + "%" )
})
// .pipe(fs.createWriteStream(os.homedir() + "/Downloads/" + filename))
.pipe(res);
})
.catch((error) => {
console.log(error);
});
});
app.listen(3000, () => {
console.log("Server: http://localhost:3000");
});

@ -0,0 +1,75 @@
<!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>Youtube Video Downloader</title>
<link rel="stylesheet" href="index.css">
<script src="index.js" defer></script>
</head>
<body>
<!-- Theme toggle -->
<div id="themeToggle" onclick="toggle()">
<div id="themeToggleInside"></div>
</div>
<h1>YouTube Downloader</h1>
<input type="text" name="url" placeholder="Paste Video URL or ID here" id="url" autofocus>
<!-- Get info button -->
<button id="getInfo" onclick="clickAnimation('getInfo')">Get info</button>
<p id="incorrectMsg"></p>
<div id="loadingWrapper">
<span>Loading</span>
<svg version="1.1" id="L4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 100 100" enable-background="new 0 0 0 0" xml:space="preserve">
<circle fill="rgb(84, 171, 222)" stroke="none" cx="6" cy="50" r="6">
<animate attributeName="opacity" dur="1s" values="0;1;0" repeatCount="indefinite" begin="0.1" />
</circle>
<circle fill="rgb(84, 171, 222)" stroke="none" cx="26" cy="50" r="6">
<animate attributeName="opacity" dur="1s" values="0;1;0" repeatCount="indefinite" begin="0.2" />
</circle>
<circle fill="rgb(84, 171, 222)" stroke="none" cx="46" cy="50" r="6">
<animate attributeName="opacity" dur="1s" values="0;1;0" repeatCount="indefinite" begin="0.3" />
</circle>
</svg>
</div>
<div id="hidden">
<div id="btnContainer">
<button class="toggleBtn" id="videoToggle">Video</button>
<button class="toggleBtn" id="audioToggle">Audio</button>
</div>
<p id="title">Title: </p>
<div id="videoList">
<form action="/download" method="post">
<label>Select Format - </label>
<select name="videoTag" id="videoFormatSelect">
</select>
<br>
<input type="hidden" name="url" class="url">
<button type="submit" class="submitBtn">Download</button>
</form>
</div>
<div id="audioList">
<form action="/download" method="post">
<label>Select Format - </label>
<select name="audioTag" id="audioFormatSelect">
</select>
<br>
<input type="hidden" name="url" class="url">
<button type="submit" class="submitBtn" id="submitBtn" onclick="clickAnimation('submitBtn')">Download</button>
</form>
</div>
</div>
</body>
</html>

@ -0,0 +1,28 @@
const { app, BrowserWindow } = require('electron')
const path = require('path')
require("./app.js")
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600
})
win.loadURL("http://localhost:3000")
}
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})

@ -0,0 +1,68 @@
{
"dependencies": {
"body-parser": "^1.20.0",
"express": "^4.18.1",
"ffmpeg-static": "^5.0.2",
"ytdl-core": "^4.11.0"
},
"name": "ytdownloader",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron .",
"windows": "electron-builder -w",
"linux": "electron-builder -l",
"mac": "electron-builder -m"
},
"keywords": [],
"author": "Andrew",
"license": "MIT",
"description": "Download videos and audios from YouTube",
"devDependencies": {
"electron": "^19.0.9",
"electron-builder": "^23.1.0"
},
"build": {
"productName": "YTDownloader",
"appId": "com.andrew.ytdownloader",
"mac": {
"category": "Utility",
"target": [
"dmg"
]
},
"dmg": {
"contents": [
{
"x": 130,
"y": 220
},
{
"x": 410,
"y": 220,
"type": "link",
"path": "/Applications"
}
],
"sign": false
},
"asar": true,
"directories": {
"buildResources": "resources",
"output": "release"
},
"linux": {
"target": [
"AppImage"
],
"category": "Utility"
},
"win": {
"target": "nsis"
},
"nsis": {
"allowToChangeInstallationDirectory": true,
"oneClick": false
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

@ -0,0 +1,149 @@
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue",
sans-serif;
text-align: center;
font-size: xx-large;
padding: 10px;
background-color: whitesmoke;
}
#title {
font-size: x-large;
font-family: sans-serif;
}
h1 {
font-size: 50px;
}
input[type="text"] {
padding: 10px;
border-radius: 10px;
border: 1px solid gray;
outline: none;
font-size: large;
width: 70%;
}
#getInfo {
padding: 10px;
background-color: rgb(64, 227, 64);
border: none;
border-radius: 8px;
color: white;
font-size: large;
cursor: pointer;
/* animation-name: clickAnimation; */
animation-duration: .5s;
animation-timing-function: linear;
}
@keyframes clickAnimation {
0% {background-color: rgb(64, 227, 64);}
50% {background-color: rgb(40, 126, 40);}
100% {background-color: rgb(64, 227, 64);}
}
#hidden {
display: none;
/* display: inline-block; */
background-color: rgb(143, 239, 207);
border-radius: 10px;
width: 80%;
padding: 10px;
color:black;
}
#btnContainer {
display: flex;
flex-direction: row;
justify-content: center;
}
.toggleBtn {
width: 50%;
font-size: x-large;
border: none;
background-color: rgb(108, 231, 190);
border-radius: 10px;
cursor: pointer;
padding: 8px;
}
select {
padding: 15px;
background-color: lightgreen;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: large;
margin: 8px;
outline:none;
}
label {
position: relative;
top: 3px;
}
#videoList,
#audioList {
display: none;
}
.submitBtn {
padding: 15px;
margin: 15px;
border-radius: 8px;
background-color: rgb(64, 227, 64);
color: white;
border: none;
font-size: large;
cursor: pointer;
animation-duration: .5s;
animation-timing-function: linear;
}
#incorrectMsg {
color: rgb(250, 59, 59);
}
#loadingWrapper{
display:none;
flex-direction: row;
justify-content: center;
align-items: center;
}
svg {
width: 100px;
height: 100px;
display: inline-block;
margin-left:20px
}
#themeToggle {
width: 55px;
height: 30px;
background-color: rgb(147, 174, 185);
border-radius: 40px;
display: flex;
cursor: pointer;
transition: linear;
transition-duration: 0.4s;
}
#themeToggleInside {
background-color: rgb(255, 255, 255);
border-radius: 30px;
width: 22px;
height: 22px;
margin: 4px;
position: relative;
transition: linear;
transition-duration: 0.4s;
left:0px;
}

@ -0,0 +1,170 @@
const videoToggle = document.getElementById("videoToggle");
const audioToggle = document.getElementById("audioToggle");
const incorrectMsg = document.getElementById("incorrectMsg");
const loadingMsg = document.getElementById("loadingWrapper")
function getInfo() {
incorrectMsg.textContent = "";
loadingMsg.style.display = "flex";
const url = document.getElementById("url").value;
const options = {
method: "POST",
body: "url=" + url,
headers: {
"Content-Type": "Application/x-www-form-urlencoded",
},
};
fetch("/", options)
.then((response) => response.json())
.then((data) => {
console.log(data.formats);
const urlElements = document.querySelectorAll(".url");
urlElements.forEach((element) => {
element.value = data.url;
});
if (data.status == true) {
loadingMsg.style.display = "none";
document.getElementById("hidden").style.display =
"inline-block";
document.getElementById("title").innerHTML =
"<b>Title</b>: " + data.title;
document.getElementById("videoList").style.display = "block";
videoToggle.style.backgroundColor = "rgb(67, 212, 164)";
for (let format of data.formats) {
let sizeSplitted = (Number(format.contentLength) / 1000000)
.toString().split(".")
let size = sizeSplitted[0] + sizeSplitted[1].slice(0,1)
size = size + " MB";
// For videos
if (format.hasVideo && format.contentLength && format.container == "mp4") {
const itag = format.itag;
const element =
"<option value='" +
itag +
"'>" +
format.qualityLabel +
" | " +
format.container +
" | " +
size +
"</option>";
document.getElementById(
"videoFormatSelect"
).innerHTML += element;
}
// For audios
else {
const pattern = /^mp*4a[0-9.]+$/g;
let audioCodec;
const itag = format.itag;
if (pattern.test(format.audioCodec)) {
audioCodec = "mp4a";
} else {
audioCodec = format.audioCodec;
}
const element =
"<option value='" +
itag +
"'>" +
format.audioBitrate +
" kbps" +
" | " +
audioCodec +
" | " +
size +
"</option>";
document.getElementById(
"audioFormatSelect"
).innerHTML += element;
}
}
} else {
loadingMsg.style.display = "none";
incorrectMsg.textContent = "Some error has occured";
}
})
.catch((error) => {
if (error) {
loadingMsg.style.display = "none";
incorrectMsg.textContent = error;
}
});
}
document.getElementById("getInfo").addEventListener("click", (event) => {
getInfo();
});
document.getElementById("url").addEventListener("keypress", (event) => {
if (event.key == "Enter") {
getInfo();
}
});
videoToggle.addEventListener("click", (event) => {
videoToggle.style.backgroundColor = "rgb(67, 212, 164)";
audioToggle.style.backgroundColor = "rgb(108, 231, 190)";
document.getElementById("audioList").style.display = "none";
document.getElementById("videoList").style.display = "block";
});
audioToggle.addEventListener("click", (event) => {
audioToggle.style.backgroundColor = "rgb(67, 212, 164)";
videoToggle.style.backgroundColor = "rgb(108, 231, 190)";
document.getElementById("videoList").style.display = "none";
document.getElementById("audioList").style.display = "block";
});
// Toggle theme
let darkTheme = false;
let button = document.getElementById("themeToggle");
let circle = document.getElementById("themeToggleInside");
function toggle() {
if (darkTheme == false) {
circle.style.left = "25px";
button.style.backgroundColor = "rgb(80, 193, 238)";
darkTheme = true;
document.body.style.backgroundColor = "rgb(50,50,50)";
document.getElementById("hidden").style.backgroundColor = "rgb(143, 239, 207)"
document.body.style.color = "whitesmoke";
localStorage.setItem("theme", "dark");
} else {
circle.style.left = "0px";
darkTheme = false;
button.style.backgroundColor = "rgb(147, 174, 185)";
document.body.style.backgroundColor = "whitesmoke";
document.getElementById("hidden").style.backgroundColor = "rgb(203, 253, 236)"
document.body.style.color = "black";
localStorage.setItem("theme", "light");
}
}
const storageTheme = localStorage.getItem("theme");
if (storageTheme == "dark") {
darkTheme = false;
toggle();
} else if (storageTheme == "light") {
darkTheme = true;
toggle();
}
////
function clickAnimation(id) {
document.getElementById(id).style.animationName = "clickAnimation";
setTimeout(() => {
document.getElementById("getInfo").style.animationName = "";
}, 500);
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Loading…
Cancel
Save