diff --git a/.gitignore b/.gitignore index b54e148..abf99cf 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,5 @@ node_modules/* backend/node_modules/* YoutubeDL-Material/node_modules/* backend/video/* -backend/audio/* \ No newline at end of file +backend/audio/* +src/assets/default.json diff --git a/backend/config/encrypted.json b/backend/config/encrypted.json index 3f8cb55..905eb81 100644 --- a/backend/config/encrypted.json +++ b/backend/config/encrypted.json @@ -18,6 +18,10 @@ "title_top": "Youtube Downloader", "download_only_mode": false, "file_manager_enabled": true + }, + "API": { + "use_youtube_API": false, + "youtube_API_key": "" } } } diff --git a/src/app/app.component.css b/src/app/app.component.css index fb3c755..d0f9435 100644 --- a/src/app/app.component.css +++ b/src/app/app.component.css @@ -53,4 +53,16 @@ mat-form-field.mat-form-field { .equal-sizes { padding-right: 20px; +} + +.search-card-title { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.input-clear-button { + position: absolute; + right: -10px; + top: 5px; } \ No newline at end of file diff --git a/src/app/app.component.html b/src/app/app.component.html index 34191bb..30afddf 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -22,9 +22,25 @@
- + Please enter a valid URL! + + + + +
+ {{result.title}} +
+
+ {{result.uploaded}} +
+
+ + +
+
+

Only Audio diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 3e1ba0e..13c9b9e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ElementRef, ViewChild } from '@angular/core'; import {PostsService} from './posts.services'; import {FileCardComponent} from './file-card/file-card.component'; import { Observable } from 'rxjs/Observable'; @@ -9,6 +9,12 @@ import { saveAs } from 'file-saver'; import 'rxjs/add/observable/of'; import 'rxjs/add/operator/mapTo'; import 'rxjs/add/operator/toPromise'; +import 'rxjs/add/observable/fromEvent' +import 'rxjs/add/operator/filter' +import 'rxjs/add/operator/debounceTime' +import 'rxjs/add/operator/do' +import 'rxjs/add/operator/switch' +import { YoutubeSearchService, Result } from './youtube-search.service'; @Component({ selector: 'app-root', @@ -33,12 +39,21 @@ export class AppComponent implements OnInit { audioFolderPath; videoFolderPath; + // youtube api + youtubeSearchEnabled = false; + youtubeAPIKey = null; + results_loading = false; + results_showing = true; + results = []; + mp3s: any[] = []; mp4s: any[] = []; urlForm = new FormControl('', [Validators.required]); - constructor(private postsService: PostsService, public snackBar: MatSnackBar) { + @ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef; + + constructor(private postsService: PostsService, private youtubeSearch: YoutubeSearchService, public snackBar: MatSnackBar) { this.audioOnly = false; @@ -51,6 +66,8 @@ export class AppComponent 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.youtubeAPIKey = this.youtubeSearchEnabled ? result['YoutubeDLMaterial']['API']['youtube_API_key'] : null; this.postsService.path = backendUrl; this.postsService.startPath = backendUrl; @@ -60,6 +77,11 @@ export class AppComponent implements OnInit { this.getMp3s(); this.getMp4s(); } + + if (this.youtubeSearchEnabled && this.youtubeAPIKey) { + this.youtubeSearch.initializeAPI(this.youtubeAPIKey); + this.attachToInput(); + } }, error => { console.log(error); }); @@ -267,6 +289,34 @@ export class AppComponent implements OnInit { }); } + clearInput() { + this.url = ''; + this.results_showing = false; + } + + onInputBlur() { + this.results_showing = false; + } + + visitURL(url) { + window.open(url); + } + + useURL(url) { + this.results_showing = false; + this.url = url; + } + + inputChanged(new_val) { + if (new_val === '') { + this.results_showing = false; + } else { + if (this.ValidURL(new_val)) { + this.results_showing = false; + } + } + } + // checks if url is a valid URL ValidURL(str) { // tslint:disable-next-line: max-line-length @@ -281,5 +331,35 @@ export class AppComponent implements OnInit { duration: 2000, }); } + + attachToInput() { + Observable.fromEvent(this.urlInput.nativeElement, 'keyup') + .map((e: any) => e.target.value) // extract the value of input + .filter((text: string) => text.length > 1) // filter out if empty + .debounceTime(250) // only once every 250ms + .do(() => this.results_loading = true) // enable loading + .map((query: string) => this.youtubeSearch.search(query)) + .switch() // act on the return of the search + .subscribe( + (results: Result[]) => { + // console.log(results); + this.results_loading = false; + if (results && results.length > 0) { + this.results = results; + this.results_showing = true; + } else { + this.results_showing = false; + } + }, + (err: any) => { + console.log(err) + this.results_loading = false; + this.results_showing = false; + }, + () => { // on completion + this.results_loading = false; + } + ); + } } diff --git a/src/app/youtube-search.service.spec.ts b/src/app/youtube-search.service.spec.ts new file mode 100644 index 0000000..d04378d --- /dev/null +++ b/src/app/youtube-search.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { YoutubeSearchService } from './youtube-search.service'; + +describe('YoutubeSearchService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: YoutubeSearchService = TestBed.get(YoutubeSearchService); + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/youtube-search.service.ts b/src/app/youtube-search.service.ts new file mode 100644 index 0000000..b0cb13f --- /dev/null +++ b/src/app/youtube-search.service.ts @@ -0,0 +1,102 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +export class Result { + id: string + title: string + desc: string + thumbnailUrl: string + videoUrl: string + uploaded: any; + + constructor(obj?: any) { + this.id = obj && obj.id || null + this.title = obj && obj.title || null + this.desc = obj && obj.desc || null + this.thumbnailUrl = obj && obj.thumbnailUrl || null + this.uploaded = obj && obj.uploaded || null + this.videoUrl = obj && obj.videoUrl || `https://www.youtube.com/watch?v=${this.id}` + + this.uploaded = formatDate(Date.parse(this.uploaded)); + } + +} + +@Injectable({ + providedIn: 'root' +}) +export class YoutubeSearchService { + + url = 'https://www.googleapis.com/youtube/v3/search'; + key = null; + + constructor(private http: HttpClient) { } + + initializeAPI(key) { + this.key = key; + } + + search(query: string): Observable { + if (this.ValidURL(query)) { + return new Observable(); + } + const params: string = [ + `q=${query}`, + `key=${this.key}`, + `part=snippet`, + `type=video`, + `maxResults=5` + ].join('&') + const queryUrl = `${this.url}?${params}` + console.log(queryUrl) + return this.http.get(queryUrl).map(response => { + return response['items'].map(item => { + return new Result({ + id: item.id.videoId, + title: item.snippet.title, + desc: item.snippet.description, + thumbnailUrl: item.snippet.thumbnails.high.url, + uploaded: item.snippet.publishedAt + }) + }) + }) + } + + // checks if url is a valid URL + ValidURL(str) { + // 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); + } +} + +function formatDate(dateVal) { + const newDate = new Date(dateVal); + + const sMonth = padValue(newDate.getMonth() + 1); + const sDay = padValue(newDate.getDate()); + const sYear = newDate.getFullYear(); + let sHour: any; + sHour = newDate.getHours(); + const sMinute = padValue(newDate.getMinutes()); + let sAMPM = 'AM'; + + const iHourCheck = parseInt(sHour, 10); + + if (iHourCheck > 12) { + sAMPM = 'PM'; + sHour = iHourCheck - 12; + } else if (iHourCheck === 0) { + sHour = '12'; + } + + sHour = padValue(sHour); + + return sMonth + '-' + sDay + '-' + sYear + ' ' + sHour + ':' + sMinute + ' ' + sAMPM; +} + +function padValue(value) { + return (value < 10) ? '0' + value : value; +}