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 @@
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;
+}