From 2a80ea92cceddebd9115f476270cbce95e28f535 Mon Sep 17 00:00:00 2001 From: Isaac Grynsztein Date: Wed, 17 Jan 2018 14:35:47 -0500 Subject: [PATCH] Updated favicon, added progress bar functionality --- backend/app.js | 111 +++++++++++++++++++++++++++++++++--- backend/config/default.json | 6 +- src/app/app.component.html | 7 ++- src/app/app.component.ts | 48 +++++++++++++--- src/app/posts.services.ts | 4 +- src/favicon.ico | Bin 5430 -> 13052 bytes 6 files changed, 155 insertions(+), 21 deletions(-) diff --git a/backend/app.js b/backend/app.js index e508616..732982a 100644 --- a/backend/app.js +++ b/backend/app.js @@ -54,24 +54,109 @@ app.post('/tomp3', function(req, res) { var date = Date.now(); var path = audioPath; var audiopath = Date.now(); - youtubedl.exec(url, ['-o', path + audiopath + ".mp3", '-x', '--audio-format', 'mp3'], {}, function(err, output) { + youtubedl.exec(url, ['-o', path + audiopath + ".mp3", '-x', '--audio-format', 'mp3', '--write-info-json'], {}, function(err, output) { if (err) { audiopath = "-1"; throw err; } }); + + // write file info + + youtubedl.getInfo(url, function(err, info) { + if (err) throw err; + + var size = info.size; + fs.writeFile("data/"+audiopath, size, function(err) { + if(err) { + return console.log(err); + } + + console.log("The file was saved!"); + }); + }); var completeString = "done"; var audiopathEncoded = encodeURIComponent(audiopath); res.send(audiopathEncoded); res.end("yes"); }); +function getFileSizeMp3(name) +{ + var jsonPath = audioPath+name+".mp3.info.json"; + + if (fs.existsSync(jsonPath)) + var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8')); + else + var obj = 0; + + return obj.filesize; +} + +function getAmountDownloadedMp3(name) +{ + var partPath = audioPath+name+".mp3.part"; + if (fs.existsSync(partPath)) + { + const stats = fs.statSync(partPath); + const fileSizeInBytes = stats.size; + return fileSizeInBytes; + } + else + return 0; +} + +function getFileSizeMp4(name) +{ + var jsonPath = videoPath+name+".info.json"; + var filesize = 0; + if (fs.existsSync(jsonPath)) + { + var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8')); + var format = obj.format.substring(0,3); + for (i = 0; i < obj.formats.length; i++) + { + if (obj.formats[i].format_id == format) + { + filesize = obj.formats[i].filesize; + } + } + } + + return filesize; +} + +function getAmountDownloadedMp4(name) +{ + var format = getVideoFormatID(name); + var partPath = videoPath+name+".f"+format+".mp4.part"; + if (fs.existsSync(partPath)) + { + const stats = fs.statSync(partPath); + const fileSizeInBytes = stats.size; + return fileSizeInBytes; + } + else + return 0; +} + +function getVideoFormatID(name) +{ + var jsonPath = videoPath+name+".info.json"; + if (fs.existsSync(jsonPath)) + { + var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8')); + var format = obj.format.substring(0,3); + return format; + } +} + app.post('/tomp4', function(req, res) { var url = req.body.url; var date = Date.now(); var path = videoPath; var videopath = Date.now(); - youtubedl.exec(url, ['-o', path + videopath + ".mp4", '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4'], {}, function(err, output) { + youtubedl.exec(url, ['-o', path + videopath + ".mp4", '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4', '--write-info-json'], {}, function(err, output) { if (err) { videopath = "-1"; throw err; @@ -88,14 +173,19 @@ app.post('/mp3fileexists', function(req, res) { var exists = ""; var fullpath = audioPath + name + ".mp3"; if (fs.existsSync(fullpath)) { - exists = basePath + audioPath + name; + exists = [basePath + audioPath + name, getFileSizeMp3(name)]; } else { - exists = "failed"; + var percent = 0; + var size = getFileSizeMp3(name); + var downloaded = getAmountDownloadedMp3(name); + if (size > 0) + percent = downloaded/size; + exists = ["failed", getFileSizeMp3(name), percent]; } //console.log(exists + " " + name); - res.send(JSON.stringify(exists)); + res.send(exists); res.end("yes"); }); @@ -104,14 +194,19 @@ app.post('/mp4fileexists', function(req, res) { var exists = ""; var fullpath = videoPath + name + ".mp4"; if (fs.existsSync(fullpath)) { - exists = basePath + videoPath + name; + exists = [basePath + videoPath + name, getFileSizeMp4(name)]; } else { - exists = "failed"; + var percent = 0; + var size = getFileSizeMp4(name); + var downloaded = getAmountDownloadedMp4(name); + if (size > 0) + percent = downloaded/size; + exists = ["failed", getFileSizeMp4(name), percent]; } //console.log(exists + " " + name); - res.send(JSON.stringify(exists)); + res.send(exists); res.end("yes"); }); diff --git a/backend/config/default.json b/backend/config/default.json index c96bcfb..3f7f2c6 100644 --- a/backend/config/default.json +++ b/backend/config/default.json @@ -7,13 +7,15 @@ "Encryption": { "use-encryption": false, "cert-file-path": "cert.pem", - "key-file-path": "privkey.pem", - "chain-file-path": "chain.pem" + "key-file-path": "privkey.pem" }, "Downloader": { "path-base": "http://localhost:8088/", "path-audio": "audio/", "path-video": "video/" + }, + "Extra": { + "title_top": "Youtube Downloader" } } } \ No newline at end of file diff --git a/src/app/app.component.html b/src/app/app.component.html index e8a94be..1ede66b 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -33,7 +33,12 @@
- +
+ +
+ + +
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 07df85f..0735951 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -3,6 +3,7 @@ import {PostsService} from './posts.services'; import { Observable } from 'rxjs/Observable'; import {FormControl, Validators} from '@angular/forms'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MatSnackBar} from '@angular/material'; import 'rxjs/add/observable/of'; import 'rxjs/add/operator/mapTo'; import 'rxjs/add/operator/toPromise'; @@ -13,6 +14,7 @@ import 'rxjs/add/operator/toPromise'; styleUrls: ['./app.component.css'] }) export class AppComponent { + determinateProgress: boolean = false; downloadingfile: boolean = false; audioOnly: boolean; urlError: boolean = false; @@ -20,13 +22,15 @@ export class AppComponent { url: string = ''; exists: string = ""; topBarTitle: string = "Youtube Downloader"; - constructor(private postsService: PostsService) { + percentDownloaded: number; + constructor(private postsService: PostsService, public snackBar: MatSnackBar) { this.audioOnly = true; - this.postsService.loadNavItems().subscribe(result => { + this.postsService.loadNavItems().subscribe(result => { // loads settings var backendUrl = result.YoutubeDLMaterial.Host.backendurl; + this.topBarTitle = result.YoutubeDLMaterial.Extra.title_top; this.postsService.path = backendUrl; this.postsService.startPath = backendUrl; @@ -66,9 +70,17 @@ export class AppComponent { downloadHelperMp3(name: string) { this.postsService.getFileStatusMp3(name).subscribe(fileExists => { - this.exists = fileExists; - if (this.exists == "failed") + var exists = fileExists; + this.exists = exists[0]; + if (exists[0] == "failed") { + var percent = exists[2]; + console.log(percent); + if (percent > 0.30) + { + this.determinateProgress = true; + this.percentDownloaded = percent*100; + } setTimeout(() => this.downloadHelperMp3(name), 500); } else @@ -82,9 +94,16 @@ export class AppComponent { downloadHelperMp4(name: string) { this.postsService.getFileStatusMp4(name).subscribe(fileExists => { - this.exists = fileExists; - if (this.exists == "failed") + var exists = fileExists; + this.exists = exists[0]; + if (exists[0] == "failed") { + var percent = exists[2]; + if (percent > 0.30) + { + this.determinateProgress = true; + this.percentDownloaded = percent*100; + } setTimeout(() => this.downloadHelperMp4(name), 500); } else @@ -111,8 +130,11 @@ export class AppComponent { { this.downloadHelperMp3(this.path); } + }, + error => { // can't access server + this.downloadingfile = false; + this.openSnackBar("Download failed!", "OK."); }); - } else { @@ -123,7 +145,11 @@ export class AppComponent { { this.downloadHelperMp4(this.path); } - }); + }, + error => { // can't access server + this.downloadingfile = false; + this.openSnackBar("Download failed!", "OK."); + }); } } else @@ -137,5 +163,11 @@ export class AppComponent { var re=new RegExp(strRegex); return re.test(str); } + + openSnackBar(message: string, action: string) { + this.snackBar.open(message, action, { + duration: 2000, + }); + } } diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 7ace913..5859fed 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -55,12 +55,12 @@ export class PostsService { .map(res => res.json()); } - getFileStatusMp3(name: string): Observable { + getFileStatusMp3(name: string): Observable { return this.http.post(this.path + "mp3fileexists",{name: name}) .map(res => res.json()); } - getFileStatusMp4(name: string): Observable { + getFileStatusMp4(name: string): Observable { return this.http.post(this.path + "mp4fileexists",{name: name}) .map(res => res.json()); } diff --git a/src/favicon.ico b/src/favicon.ico index 8081c7ceaf2be08bf59010158c586170d9d2d517..86ee06e33f62231347f0cd2fb5bd7d3b872fb9d3 100644 GIT binary patch literal 13052 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 literal 5430 zcmc(je{54#6vvCoAI3i*G5%$U7!sA3wtMZ$fH6V9C`=eXGJb@R1%(I_{vnZtpD{6n z5Pl{DmxzBDbrB>}`90e12m8T*36WoeDLA&SD_hw{H^wM!cl_RWcVA!I+x87ee975; z@4kD^=bYPn&pmG@(+JZ`rqQEKxW<}RzhW}I!|ulN=fmjVi@x{p$cC`)5$a!)X&U+blKNvN5tg=uLvuLnuqRM;Yc*swiexsoh#XPNu{9F#c`G zQLe{yWA(Y6(;>y|-efAy11k<09(@Oo1B2@0`PtZSkqK&${ zgEY}`W@t{%?9u5rF?}Y7OL{338l*JY#P!%MVQY@oqnItpZ}?s z!r?*kwuR{A@jg2Chlf0^{q*>8n5Ir~YWf*wmsh7B5&EpHfd5@xVaj&gqsdui^spyL zB|kUoblGoO7G(MuKTfa9?pGH0@QP^b#!lM1yHWLh*2iq#`C1TdrnO-d#?Oh@XV2HK zKA{`eo{--^K&MW66Lgsktfvn#cCAc*(}qsfhrvOjMGLE?`dHVipu1J3Kgr%g?cNa8 z)pkmC8DGH~fG+dlrp(5^-QBeEvkOvv#q7MBVLtm2oD^$lJZx--_=K&Ttd=-krx(Bb zcEoKJda@S!%%@`P-##$>*u%T*mh+QjV@)Qa=Mk1?#zLk+M4tIt%}wagT{5J%!tXAE;r{@=bb%nNVxvI+C+$t?!VJ@0d@HIyMJTI{vEw0Ul ze(ha!e&qANbTL1ZneNl45t=#Ot??C0MHjjgY8%*mGisN|S6%g3;Hlx#fMNcL<87MW zZ>6moo1YD?P!fJ#Jb(4)_cc50X5n0KoDYfdPoL^iV`k&o{LPyaoqMqk92wVM#_O0l z09$(A-D+gVIlq4TA&{1T@BsUH`Bm=r#l$Z51J-U&F32+hfUP-iLo=jg7Xmy+WLq6_tWv&`wDlz#`&)Jp~iQf zZP)tu>}pIIJKuw+$&t}GQuqMd%Z>0?t%&BM&Wo^4P^Y z)c6h^f2R>X8*}q|bblAF?@;%?2>$y+cMQbN{X$)^R>vtNq_5AB|0N5U*d^T?X9{xQnJYeU{ zoZL#obI;~Pp95f1`%X3D$Mh*4^?O?IT~7HqlWguezmg?Ybq|7>qQ(@pPHbE9V?f|( z+0xo!#m@Np9PljsyxBY-UA*{U*la#8Wz2sO|48_-5t8%_!n?S$zlGe+NA%?vmxjS- zHE5O3ZarU=X}$7>;Okp(UWXJxI%G_J-@IH;%5#Rt$(WUX?6*Ux!IRd$dLP6+SmPn= z8zjm4jGjN772R{FGkXwcNv8GBcZI#@Y2m{RNF_w8(Z%^A*!bS*!}s6sh*NnURytky humW;*g7R+&|Ledvc-