From 5f4a7a1e694df176cc08c2e93bf0c9388e3e3008 Mon Sep 17 00:00:00 2001 From: Isaac Grynsztein Date: Mon, 17 Feb 2020 00:32:31 -0500 Subject: [PATCH] Added support for custom quality settings for video and audio files. Available formats are downloaded when a valid YT url is detected. These formats are then parsed and a best audio format is selected based on the results After downloading a file with no file manager, file is now deleted. After file deletion, mp3/mp4 reload occurs Updated view on main component to be more responsive, using bootstrap grid Updated progress bar UI-wise to be more in line with the rest of the page --- src/app/main/main.component.css | 16 ++ src/app/main/main.component.html | 53 +++++-- src/app/main/main.component.ts | 260 +++++++++++++++++++++++++++++-- 3 files changed, 297 insertions(+), 32 deletions(-) diff --git a/src/app/main/main.component.css b/src/app/main/main.component.css index d0f9435..98e1899 100644 --- a/src/app/main/main.component.css +++ b/src/app/main/main.component.css @@ -36,6 +36,10 @@ mat-toolbar.top { width: 100%; } +.example-80-width { + width: 80% +} + mat-form-field.mat-form-field { font-size: 24px; } @@ -65,4 +69,16 @@ mat-form-field.mat-form-field { position: absolute; right: -10px; top: 5px; +} + +.spinner-div { + display: inline-block; + position: absolute; + top: 15px; + right: -40px; +} + +.margined { + margin-left: 20px; + margin-right: 20px; } \ No newline at end of file diff --git a/src/app/main/main.component.html b/src/app/main/main.component.html index f173278..65a3a4e 100644 --- a/src/app/main/main.component.html +++ b/src/app/main/main.component.html @@ -7,11 +7,32 @@
- - - Please enter a valid URL! - - +
+
+
+ + + Please enter a valid URL! + + +
+
+ + Quality + + + + {{option.label}} + + + +
+ +
+
+
+
+
@@ -29,7 +50,7 @@
- Only Audio + Only Audio
@@ -41,16 +62,18 @@
-
- -
+
+
+ +
+
+
+ +
+ + +
-
- -
- - -
diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts index b9a5b7c..896b8c8 100644 --- a/src/app/main/main.component.ts +++ b/src/app/main/main.component.ts @@ -39,6 +39,8 @@ export class MainComponent implements OnInit { audioFolderPath; videoFolderPath; + cachedAvailableFormats = {}; + // youtube api youtubeSearchEnabled = false; youtubeAPIKey = null; @@ -52,7 +54,104 @@ export class MainComponent implements OnInit { urlForm = new FormControl('', [Validators.required]); + qualityOptions = { + 'video': [ + { + 'resolution': null, + 'value': '', + 'label': 'Max' + }, + { + 'resolution': '3840x2160', + 'value': '2160', + 'label': '2160p (4K)' + }, + { + 'resolution': '2560x1440', + 'value': '1440', + 'label': '1440p' + }, + { + 'resolution': '1920x1080', + 'value': '1080', + 'label': '1080p' + }, + { + 'resolution': '1280x720', + 'value': '720', + 'label': '720p' + }, + { + 'resolution': '720x480', + 'value': '480', + 'label': '480p' + }, + { + 'resolution': '480x360', + 'value': '360', + 'label': '360p' + }, + { + 'resolution': '360x240', + 'value': '240', + 'label': '240p' + }, + { + 'resolution': '256x144', + 'value': '144', + 'label': '144p' + } + ], + 'audio': [ + { + 'kbitrate': null, + 'value': '', + 'label': 'Max' + }, + { + 'kbitrate': '256', + 'value': '256K', + 'label': '256 Kbps' + }, + { + 'kbitrate': '160', + 'value': '160K', + 'label': '160 Kbps' + }, + { + 'kbitrate': '128', + 'value': '128K', + 'label': '128 Kbps' + }, + { + 'kbitrate': '96', + 'value': '96K', + 'label': '96 Kbps' + }, + { + 'kbitrate': '70', + 'value': '70K', + 'label': '70 Kbps' + }, + { + 'kbitrate': '50', + 'value': '50K', + 'label': '50 Kbps' + }, + { + 'kbitrate': '32', + 'value': '32K', + 'label': '32 Kbps' + } + ] + } + + selectedQuality = ''; + formats_loading = false; + @ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef; + last_valid_url = ''; + last_url_check = 0; constructor(private postsService: PostsService, private youtubeSearch: YoutubeSearchService, public snackBar: MatSnackBar, private router: Router) { @@ -67,7 +166,8 @@ export class MainComponent implements OnInit { this.baseStreamPath = result['YoutubeDLMaterial']['Downloader']['path-base']; this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio']; this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video']; - this.youtubeSearchEnabled = result['YoutubeDLMaterial']['API'] && result['YoutubeDLMaterial']['API']['use_youtube_API']; + this.youtubeSearchEnabled = result['YoutubeDLMaterial']['API'] && result['YoutubeDLMaterial']['API']['use_youtube_API'] && + result['YoutubeDLMaterial']['API']['youtube_API_key']; this.youtubeAPIKey = this.youtubeSearchEnabled ? result['YoutubeDLMaterial']['API']['youtube_API_key'] : null; this.postsService.path = backendUrl; @@ -150,17 +250,18 @@ export class MainComponent implements OnInit { if (forceView === false && this.downloadOnlyMode && !this.iOS) { if (is_playlist) { for (let i = 0; i < name.length; i++) { - this.downloadAudioFile(name[i]); + this.downloadAudioFile(decodeURI(name[i])); } } else { - this.downloadAudioFile(name); + this.downloadAudioFile(decodeURI(name)); } } else { if (is_playlist) { this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'audio'}]); // window.location.href = this.baseStreamPath + this.audioFolderPath + name[0] + '.mp3'; } else { - window.location.href = this.baseStreamPath + this.audioFolderPath + name + '.mp3'; + this.router.navigate(['/player', {fileNames: name, type: 'audio'}]); + // window.location.href = this.baseStreamPath + this.audioFolderPath + name + '.mp3'; } } @@ -177,10 +278,10 @@ export class MainComponent implements OnInit { if (forceView === false && this.downloadOnlyMode) { if (is_playlist) { for (let i = 0; i < name.length; i++) { - this.downloadVideoFile(name[i]); + this.downloadVideoFile(decodeURI(name[i])); } } else { - this.downloadVideoFile(name); + this.downloadVideoFile(decodeURI(name)); } } else { if (is_playlist) { @@ -206,7 +307,17 @@ export class MainComponent implements OnInit { if (this.audioOnly) { this.downloadingfile = true; - this.postsService.makeMP3(this.url).subscribe(posts => { + + let customQualityConfiguration = null; + if (this.selectedQuality !== '') { + const cachedFormatsExists = this.cachedAvailableFormats[this.url]; + if (cachedFormatsExists) { + const audio_formats = this.cachedAvailableFormats[this.url]['audio']; + customQualityConfiguration = audio_formats[this.selectedQuality]['format_id']; + } + } + this.postsService.makeMP3(this.url, (this.selectedQuality === '' ? null : this.selectedQuality), + customQualityConfiguration).subscribe(posts => { const is_playlist = !!(posts['file_names']); this.path = is_playlist ? posts['file_names'] : posts['audiopathEncoded']; if (this.path !== '-1') { @@ -217,8 +328,20 @@ export class MainComponent implements OnInit { this.openSnackBar('Download failed!', 'OK.'); }); } else { + let customQualityConfiguration = null; + const cachedFormatsExists = this.cachedAvailableFormats[this.url]; + if (cachedFormatsExists) { + const video_formats = this.cachedAvailableFormats[this.url]['video']; + if (video_formats['best_audio_format'] && this.selectedQuality !== ''/* && + video_formats[this.selectedQuality]['acodec'] === 'none'*/) { + console.log(this.selectedQuality); + customQualityConfiguration = video_formats[this.selectedQuality]['format_id'] + '+' + video_formats['best_audio_format']; + } + } + this.downloadingfile = true; - this.postsService.makeMP4(this.url).subscribe(posts => { + this.postsService.makeMP4(this.url, (this.selectedQuality === '' ? null : this.selectedQuality), + customQualityConfiguration).subscribe(posts => { const is_playlist = !!(posts['file_names']); this.path = is_playlist ? posts['file_names'] : posts['videopathEncoded']; if (this.path !== '-1') { @@ -239,10 +362,13 @@ export class MainComponent implements OnInit { const blob: Blob = res; saveAs(blob, name + '.mp3'); - // tell server to delete the file once downloaded - this.postsService.deleteFile(name, true).subscribe(delRes => { - - }); + if (!this.fileManagerEnabled) { + // tell server to delete the file once downloaded + this.postsService.deleteFile(name, true).subscribe(delRes => { + // reload mp3s + this.getMp3s(); + }); + } }); } @@ -251,10 +377,13 @@ export class MainComponent implements OnInit { const blob: Blob = res; saveAs(blob, name + '.mp4'); - // tell server to delete the file once downloaded - this.postsService.deleteFile(name, false).subscribe(delRes => { - - }); + if (!this.fileManagerEnabled) { + // tell server to delete the file once downloaded + this.postsService.deleteFile(name, false).subscribe(delRes => { + // reload mp4s + this.getMp4s(); + }); + } }); } @@ -291,7 +420,22 @@ export class MainComponent implements OnInit { // tslint:disable-next-line: max-line-length const strRegex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/; const re = new RegExp(strRegex); - return re.test(str); + const valid = re.test(str); + + if (!valid) { return false; } + + // tslint:disable-next-line: max-line-length + const youtubeStrRegex = /(?:http(?:s)?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:(?:watch)?\?(?:.*&)?v(?:i)?=|(?:embed|v|vi|user)\/))([^\?&\"'<> #]+)/; + const reYT = new RegExp(youtubeStrRegex); + const ytValid = reYT.test(str); + if (valid && ytValid && Date.now() - this.last_url_check > 1000) { + if (str !== this.last_valid_url) { + // get info + this.getURLInfo(str); + } + this.last_valid_url = str; + } + return valid; } // snackbar helper @@ -301,6 +445,22 @@ export class MainComponent implements OnInit { }); } + getURLInfo(url) { + console.log(this.cachedAvailableFormats[url]); + if (!(this.cachedAvailableFormats[url])) { + this.formats_loading = true; + console.log('has no cached formats available'); + this.postsService.getFileInfo([url], 'irrelevant', true).subscribe(res => { + if (url === this.url) { this.formats_loading = false; } + const infos = res['result']; + const parsed_infos = this.getAudioAndVideoFormats(infos.formats); + console.log('got formats for ' + url); + const available_formats = {audio: parsed_infos[0], video: parsed_infos[1]}; + this.cachedAvailableFormats[url] = available_formats; + }); + } + } + attachToInput() { Observable.fromEvent(this.urlInput.nativeElement, 'keyup') .map((e: any) => e.target.value) // extract the value of input @@ -334,5 +494,71 @@ export class MainComponent implements OnInit { onResize(event) { this.files_cols = (event.target.innerWidth <= 450) ? 2 : 4; } + + videoModeChanged(new_val) { + this.selectedQuality = ''; + } + + getAudioAndVideoFormats(formats): any[] { + const audio_formats = {}; + const video_formats = {}; + + for (let i = 0; i < formats.length; i++) { + const format_obj = {type: null}; + + const format = formats[i]; + const format_type = (format.vcodec === 'none') ? 'audio' : 'video'; + + format_obj.type = format_type; + // console.log(format); + if (format_obj.type === 'audio' && format.abr) { + const key = format.abr.toString() + 'K'; + format_obj['bitrate'] = format.abr; + format_obj['format_id'] = format.format_id; + format_obj['ext'] = format.ext; + // don't overwrite if not m4a + if (audio_formats[key]) { + if (format.ext === 'm4a') { + audio_formats[key] = format_obj; + } + } else { + audio_formats[key] = format_obj; + } + } else if (format_obj.type === 'video') { + // check if video format is mp4 + const key = format.height.toString(); + if (format.ext === 'mp4') { + format_obj['height'] = format.height; + format_obj['acodec'] = format.acodec; + format_obj['format_id'] = format.format_id; + + // no acodec means no overwrite + if (!(video_formats[key]) || format_obj['acodec'] !== 'none') { + video_formats[key] = format_obj; + } + } + } + } + + video_formats['best_audio_format'] = this.getBestAudioFormatForMp4(audio_formats); + + return [audio_formats, video_formats] + } + + getBestAudioFormatForMp4(audio_formats) { + let best_audio_format_for_mp4 = null; + let best_audio_format_bitrate = 0; + const available_audio_format_keys = Object.keys(audio_formats); + for (let i = 0; i < available_audio_format_keys.length; i++) { + const audio_format_key = available_audio_format_keys[i]; + const audio_format = audio_formats[audio_format_key]; + const is_m4a = audio_format.ext === 'm4a'; + if (is_m4a && audio_format.bitrate > best_audio_format_bitrate) { + best_audio_format_for_mp4 = audio_format.format_id; + best_audio_format_bitrate = audio_format.bitrate; + } + } + return best_audio_format_for_mp4; + } }