Initial commit (1.0.0)

This commit is contained in:
jely2002
2020-04-02 03:25:36 +02:00
commit f184d5a8e7
12 changed files with 2867 additions and 0 deletions

BIN
bin/ffmpeg.exe Normal file

Binary file not shown.

BIN
bin/icon-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
bin/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
bin/waiting-for-link.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

140
custom.css Normal file
View File

@@ -0,0 +1,140 @@
body {
width: 100%;
height: 100%;
background-color: #212121;
color: #fff;
}
.container-fluid {
padding: 10px 30px;
}
.menubar {
display: none !important;
}
.main-input {
background-color: #303030;
padding: 30px;
margin-top: 20px;
border-radius: 3px;
}
#main_input label, .header {
font-size: 22px;
margin-bottom: 20px;
}
#main_input .form-group {
margin-bottom: 2rem;
}
.thumbnail {
border-radius: 3px;
width: 100%;
height: 21vw;
object-fit: cover;
}
.title, .channel, .duration {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.spinner-border {
display: none;
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
width: 3rem;
height: 3rem;
margin: auto;
}
.bs-stepper {
width: 100%;
}
.bs-stepper .line {
min-height: 2px;
background-color: rgba(0,0,0,.25);
}
.active .bs-stepper-circle {
background-color: #5cb85c !important;
}
/* CUSTOM CHECKMARK SPINNER */
.circle-loader {
margin-bottom: 3.5em;
border: 2px solid rgba(0, 0, 0, 0.2);
border-left-color: #5cb85c;
animation: loader-spin 1.2s infinite linear;
position: relative;
display: inline-block;
vertical-align: top;
border-radius: 50%;
width: 7em;
height: 7em;
}
.load-complete {
-webkit-animation: none;
animation: none;
border-color: #5cb85c;
transition: border 500ms ease-out;
}
.checkmark {
display: none;
}
.checkmark.draw:after {
animation-duration: 800ms;
animation-timing-function: ease;
animation-name: checkmark;
transform: scaleX(-1) rotate(135deg);
}
.checkmark:after {
opacity: 1;
height: 3.5em;
width: 1.75em;
transform-origin: left top;
border-right: 4px solid #5cb85c;
border-top: 4px solid #5cb85c;
content: '';
left: 1.65em;
top: 3.5em;
position: absolute;
}
@keyframes loader-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes checkmark {
0% {
height: 0;
width: 0;
opacity: 1;
}
20% {
height: 0;
width: 1.75em;
opacity: 1;
}
40% {
height: 3.5em;
width: 1.75em;
opacity: 1;
}
100% {
height: 3.5em;
width: 1.75em;
opacity: 1;
}
}

119
downloader.js Normal file
View File

@@ -0,0 +1,119 @@
const youtubedl = require('youtube-dl')
const {remote} = require('electron')
let selectedURL
let availableFormats = []
youtubedl.setYtdlBinary("resources/app.asar.unpacked/node_modules/youtube-dl/bin/youtube-dl.exe")
function settings() {
stepper.next()
selectedURL = $("#url").val()
}
function url_entered() {
let url = $("#url").val()
if(validate(url)) {
showInfo(url)
$('#url').addClass("is-valid").removeClass("is-invalid")
} else {
$('#url').addClass("is-invalid").removeClass("is-valid")
}
}
function validate(url) {
const regex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/gi
return (regex.test(url))
}
function showInfo(url) {
$(".spinner-border").css("display", "inherit");
youtubedl.getInfo(url, function(err, info) {
if (err) throw err
$(".thumbnail").attr("src", info.thumbnail)
$(".title").html("<strong>Title:</strong> " + info.title)
$(".channel").html("<strong>Channel:</strong> " + info.uploader)
$(".duration").html("<strong>Duration:</strong> " + info.duration)
$(".spinner-border").css("display", "none")
$('#step-one-btn').prop("disabled", false)
});
selectedURL = url
youtubedl.exec(selectedURL, ['-F','--skip-download'], {}, function(err, output) {
if (err) throw err
output.splice(0,3)
console.log(output)
output.forEach(function(entry) {
if(!(entry.includes('mp4'))) return
if(entry.includes('4320p') && !availableFormats.includes('4320p')) availableFormats.push('4320p')
if(entry.includes('2160p') && !availableFormats.includes('2160p')) availableFormats.push('2160p')
if(entry.includes('1440p') && !availableFormats.includes('1440p')) availableFormats.push('1440p')
if(entry.includes('1080p') && !availableFormats.includes('1080p')) availableFormats.push('1080p')
if(entry.includes('720p') && !availableFormats.includes('720p')) availableFormats.push('720p')
if(entry.includes('480p') && !availableFormats.includes('480p')) availableFormats.push('480p')
if(entry.includes('360p') && !availableFormats.includes('360p')) availableFormats.push('360p')
if(entry.includes('240p') && !availableFormats.includes('240p')) availableFormats.push('240p')
if(entry.includes('144p') && !availableFormats.includes('144p')) availableFormats.push('144p')
})
})
}
function downloadAudio(quality) {
const options = [
'-f', quality + 'audio[ext=m4a]',
'--ffmpeg-location', 'bin/ffmpeg.exe', '--hls-prefer-ffmpeg',
'-o', remote.app.getPath('music').replace(/\\/g, "/") + '/' + '%(title)s.%(ext)s'
]
youtubedl.exec(selectedURL, options, {}, function(err, output) {
if (err) throw err
downloadFinished()
console.log(output)
})
}
function downloadVideo(quality) {
const options = [
'-f', 'bestvideo[height<='+ quality + ',ext=mp4]+bestaudio[ext=m4a]/best[height<=' + quality + ']',
'--ffmpeg-location', 'ffmpeg.exe', '--hls-prefer-ffmpeg',
'--merge-output-format', 'mp4',
'-o', remote.app.getPath('videos').replace(/\\/g, "/") + '/' + '%(title)s.%(ext)s'
]
youtubedl.exec(selectedURL, options, {}, function(err, output) {
if (err) throw err
downloadFinished()
console.log(output)
})
}
function download() {
let quality = $('#quality').val()
stepper.next()
if($('input[name=type-select]:checked').val() === "video") {
downloadVideo(quality)
} else {
downloadAudio(quality)
}
}
function downloadFinished() {
$('.circle-loader').toggleClass('load-complete')
$('.checkmark').toggle()
$('#reset-btn').html("Download another video").prop("disabled", false)
}
function resetSteps() {
selectedURL = ""
availableFormats = []
$('#url').removeClass("is-valid").removeClass("is-invalid")
$(".thumbnail").attr("src", "https://via.placeholder.com/640x360?text=%20")
$(".title").html("<strong>Title:</strong> --")
$(".channel").html("<strong>Channel:</strong> --")
$(".duration").html("<strong>Duration:</strong> --")
$('.main-input').trigger('reset')
$('.circle-loader').toggleClass('load-complete')
$('.checkmark').toggle()
$('#reset-btn').html("Downloading...").prop("disabled", true)
$('#quality').empty().append(new Option("Select quality", "quality")).prop("disabled", true).val("quality")
stepper.reset()
}

120
index.html Normal file
View File

@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Youtube Downloader</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bs-stepper/dist/css/bs-stepper.min.css">
<link rel="stylesheet" href="node_modules/@fortawesome/fontawesome-free/css/all.min.css">
<link rel="stylesheet" href="custom.css" crossorigin="anonymous">
</head>
<body>
<div class="container-fluid text-center">
<div class="row">
<div class="col-md-12">
<div class="bs-stepper">
<div class="bs-stepper-header" role="tablist">
<div class="step" data-target="#url-part">
<button type="button" class="step-trigger" role="tab" aria-controls="url-part" id="url-part-trigger">
<span class="bs-stepper-circle"><i class="fas fa-link"></i></span>
<span class="bs-stepper-label">YouTube URL</span>
</button>
</div>
<div class="line"></div>
<div class="step" data-target="#settings-part">
<button type="button" class="step-trigger" role="tab" aria-controls="settings-part" id="settings-part-trigger">
<span class="bs-stepper-circle"><i class="fas fa-cog"></i></span>
<span class="bs-stepper-label">Settings</span>
</button>
</div>
<div class="line"></div>
<div class="step" data-target="#download-part">
<button type="button" class="step-trigger" role="tab" aria-controls="download-part" id="download-part-trigger">
<span class="bs-stepper-circle"><i class="fas fa-download"></i></span>
<span class="bs-stepper-label">Download</span>
</button>
</div>
</div>
<div class="bs-stepper-content">
<div id="url-part" class="content fade" role="tabpanel" aria-labelledby="url-part-trigger">
<form class="main-input">
<div class="form-group">
<input type="text" oninput="url_entered($(this).val())" class="form-control" id="url" placeholder="YouTube link ex. youtube.com/watch?v=FtveSk1N7Uo" required>
<div class="invalid-feedback">
Please enter a valid YouTube URL.
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="spinner-border text-dark" role="status">
<span class="sr-only">Loading...</span>
</div>
<img src="bin/waiting-for-link.png" class="img-fluid thumbnail" alt="Thumbnail of the specified video">
</div>
<div class="col-md-6 text-left">
<p class="title"><strong>Title:</strong> --</p>
<p class="channel"><strong>Channel:</strong> --</p>
<p class="duration"><strong>Duration:</strong> --</p>
<div class="row">
<div class="col-md-12">
<button type="button" id="step-one-btn" onclick="settings()" class="btn btn-dark mt-2 mb-0" disabled>Next</button>
</div>
</div>
</div>
</div>
</form>
</div>
<div id="settings-part" class="content fade" role="tabpanel" aria-labelledby="settings-part-trigger">
<form class="main-input">
<div class="row">
<div class="col-md-6">
<img src="bin/waiting-for-link.png" class="img-fluid thumbnail" alt="Thumbnail of the specified video">
</div>
<div class="col-md-6 text-left">
<div class="form-group">
<form id="settings">
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="video" onclick="setType('video')" value="video" name="type-select" class="custom-control-input">
<label class="custom-control-label" for="video">Video</label>
</div>
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" onclick="setType('audio')" id="audio" value="audio" name="type-select" class="custom-control-input">
<label class="custom-control-label" for="audio">Audio</label>
</div>
<div class="form-group">
<label for="quality"></label>
<select id="quality" class="custom-select" disabled>
<option selected>Select quality</option>
</select>
</div>
</form>
</div>
<button type="button" onclick="stepper.reset()" class="btn btn-dark">Back</button>
<button type="button" onclick="download()" class="btn btn-dark">Download</button>
</div>
</div>
</form>
</div>
<div id="download-part" class="content fade" role="tabpanel" aria-labelledby="download-part-trigger">
<div class="main-input">
<div class="row justify-content-center">
<div class="circle-loader">
<div class="checkmark draw"></div>
</div>
</div>
<div class="row justify-content-center">
<button type="button" id="reset-btn" onclick="resetSteps()" class="btn btn-dark mt-2 mb-0 col-md-4" disabled>Downloading...</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="plugins.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bs-stepper@1.7.0/dist/js/bs-stepper.min.js" integrity="sha256-INfYp5owpb0btFquNHGlhSxgGYrFlGYRU2oN/3jWGeM=" crossorigin="anonymous"></script>
<script src="downloader.js"></script>
</body>
</html>

43
main.js Normal file
View File

@@ -0,0 +1,43 @@
const { app, BrowserWindow } = require('electron')
let win
function createWindow () {
app.allowRendererProcessReuse = true
win = new BrowserWindow({
show: false,
width: 800, //850
height: 500, //550
resizable: false,
frame: false,
icon: "bin/icon-light.png",
webPreferences: {
nodeIntegration: true
}
})
win.removeMenu()
//win.webContents.openDevTools()
win.loadFile('index.html')
win.on('closed', () => {
win = null
})
win.once('ready-to-show', () => {
win.show()
})
}
app.on('ready', createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
});
app.on('activate', () => {
if (win === null) {
createWindow()
}
});

2370
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "youtube-dl-gui",
"version": "1.0.0",
"description": "A GUI for the YouTube-dl library",
"main": "main.js",
"scripts": {
"start": "electron .",
"dist": "electron-builder"
},
"keywords": [],
"author": "Jelle Glebbeek",
"license": "MIT",
"devDependencies": {
"electron": "^8.2.0",
"electron-builder": "^22.4.1"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",
"bootstrap-material-design": "^4.1.2",
"custom-electron-titlebar": "^3.2.2-hotfix62",
"fs": "0.0.1-security",
"jquery": "^3.4.1",
"popper.js": "^1.16.1",
"youtube-dl": "^3.0.2"
},
"repository": {
"type": "git",
"url": "yt-dl-gui"
},
"build": {
"appId": "com.jelleglebbeek.youtube-dl-gui",
"productName": "YouTube Downloader GUI",
"copyright": "Copyright © 2020 Jelle Glebbeek",
"win": {
"target": "portable",
"icon": "build/icon.ico"
}
}
}

36
plugins.js Normal file
View File

@@ -0,0 +1,36 @@
window.$ = window.jQuery = require('jquery')
const customTitlebar = require('custom-electron-titlebar')
let stepper
new customTitlebar.Titlebar({
backgroundColor: customTitlebar.Color.fromHex('#000000'),
maximizable: false,
shadow: true,
titleHorizontalAlignment: "left",
enableMnemonics: false,
icon: "bin/icon-light.png"
})
$(document).ready(function () {
stepper = new Stepper($('.bs-stepper')[0], {
linear: true,
animation: true
})
})
function next() {
stepper.next()
}
function setType(type) {
if(type === "audio") {
$('#quality').empty().append(new Option("Best", "best")).append(new Option("Worst", "worst")).prop("disabled", false).val("best")
} else if(type === "video") {
$('#quality').empty()
availableFormats.forEach(function(quality) {
$('#quality').append(new Option(quality, quality.slice(0,-1))).prop("disabled", false)
})
$('#quality').val(availableFormats[availableFormats.length-1].slice(0,-1))
}
}