mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-04-18 18:01:30 +03:00
added youtube search functionality in frontend
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -45,4 +45,5 @@ node_modules/*
|
|||||||
backend/node_modules/*
|
backend/node_modules/*
|
||||||
YoutubeDL-Material/node_modules/*
|
YoutubeDL-Material/node_modules/*
|
||||||
backend/video/*
|
backend/video/*
|
||||||
backend/audio/*
|
backend/audio/*
|
||||||
|
src/assets/default.json
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
"title_top": "Youtube Downloader",
|
"title_top": "Youtube Downloader",
|
||||||
"download_only_mode": false,
|
"download_only_mode": false,
|
||||||
"file_manager_enabled": true
|
"file_manager_enabled": true
|
||||||
|
},
|
||||||
|
"API": {
|
||||||
|
"use_youtube_API": false,
|
||||||
|
"youtube_API_key": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,4 +53,16 @@ mat-form-field.mat-form-field {
|
|||||||
|
|
||||||
.equal-sizes {
|
.equal-sizes {
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card-title {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-clear-button {
|
||||||
|
position: absolute;
|
||||||
|
right: -10px;
|
||||||
|
top: 5px;
|
||||||
}
|
}
|
||||||
@@ -22,9 +22,25 @@
|
|||||||
<div style="position: relative;">
|
<div style="position: relative;">
|
||||||
<form class="example-form">
|
<form class="example-form">
|
||||||
<mat-form-field class="example-full-width">
|
<mat-form-field class="example-full-width">
|
||||||
<input matInput [(ngModel)]="url" placeholder="URL" type="url" name="url" [formControl]="urlForm" required>
|
<input matInput (ngModelChange)="inputChanged($event)" [(ngModel)]="url" placeholder="URL" type="url" name="url" [formControl]="urlForm" required #urlinput>
|
||||||
<mat-error *ngIf="urlError || urlForm.invalid">Please enter a valid URL!</mat-error>
|
<mat-error *ngIf="urlError || urlForm.invalid">Please enter a valid URL!</mat-error>
|
||||||
|
<button class="input-clear-button" mat-icon-button (click)="clearInput()"><mat-icon>clear</mat-icon></button>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
<span *ngIf="results_showing">
|
||||||
|
<span *ngFor="let result of results">
|
||||||
|
<mat-card style="height: 120px; border-radius: 0px">
|
||||||
|
<div class="search-card-title">
|
||||||
|
{{result.title}}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 12px">
|
||||||
|
{{result.uploaded}}
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
|
<button mat-flat-button color="primary" style="float: left;" (click)="useURL(result.videoUrl)">Use URL</button>
|
||||||
|
<button mat-stroked-button color="primary" (click)="visitURL(result.videoUrl)" style="float: right">View</button>
|
||||||
|
</mat-card>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</form>
|
</form>
|
||||||
<br/>
|
<br/>
|
||||||
<mat-checkbox [(ngModel)]="audioOnly" style="float: left; margin-top: -12px">Only Audio</mat-checkbox>
|
<mat-checkbox [(ngModel)]="audioOnly" style="float: left; margin-top: -12px">Only Audio</mat-checkbox>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';
|
||||||
import {PostsService} from './posts.services';
|
import {PostsService} from './posts.services';
|
||||||
import {FileCardComponent} from './file-card/file-card.component';
|
import {FileCardComponent} from './file-card/file-card.component';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
@@ -9,6 +9,12 @@ import { saveAs } from 'file-saver';
|
|||||||
import 'rxjs/add/observable/of';
|
import 'rxjs/add/observable/of';
|
||||||
import 'rxjs/add/operator/mapTo';
|
import 'rxjs/add/operator/mapTo';
|
||||||
import 'rxjs/add/operator/toPromise';
|
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({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@@ -33,12 +39,21 @@ export class AppComponent implements OnInit {
|
|||||||
audioFolderPath;
|
audioFolderPath;
|
||||||
videoFolderPath;
|
videoFolderPath;
|
||||||
|
|
||||||
|
// youtube api
|
||||||
|
youtubeSearchEnabled = false;
|
||||||
|
youtubeAPIKey = null;
|
||||||
|
results_loading = false;
|
||||||
|
results_showing = true;
|
||||||
|
results = [];
|
||||||
|
|
||||||
mp3s: any[] = [];
|
mp3s: any[] = [];
|
||||||
mp4s: any[] = [];
|
mp4s: any[] = [];
|
||||||
|
|
||||||
urlForm = new FormControl('', [Validators.required]);
|
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;
|
this.audioOnly = false;
|
||||||
|
|
||||||
|
|
||||||
@@ -51,6 +66,8 @@ export class AppComponent implements OnInit {
|
|||||||
this.baseStreamPath = result['YoutubeDLMaterial']['Downloader']['path-base'];
|
this.baseStreamPath = result['YoutubeDLMaterial']['Downloader']['path-base'];
|
||||||
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
|
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
|
||||||
this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video'];
|
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.path = backendUrl;
|
||||||
this.postsService.startPath = backendUrl;
|
this.postsService.startPath = backendUrl;
|
||||||
@@ -60,6 +77,11 @@ export class AppComponent implements OnInit {
|
|||||||
this.getMp3s();
|
this.getMp3s();
|
||||||
this.getMp4s();
|
this.getMp4s();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.youtubeSearchEnabled && this.youtubeAPIKey) {
|
||||||
|
this.youtubeSearch.initializeAPI(this.youtubeAPIKey);
|
||||||
|
this.attachToInput();
|
||||||
|
}
|
||||||
}, error => {
|
}, error => {
|
||||||
console.log(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
|
// checks if url is a valid URL
|
||||||
ValidURL(str) {
|
ValidURL(str) {
|
||||||
// tslint:disable-next-line: max-line-length
|
// tslint:disable-next-line: max-line-length
|
||||||
@@ -281,5 +331,35 @@ export class AppComponent implements OnInit {
|
|||||||
duration: 2000,
|
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;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
src/app/youtube-search.service.spec.ts
Normal file
12
src/app/youtube-search.service.spec.ts
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
102
src/app/youtube-search.service.ts
Normal file
102
src/app/youtube-search.service.ts
Normal file
@@ -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<Result[]> {
|
||||||
|
if (this.ValidURL(query)) {
|
||||||
|
return new Observable<Result[]>();
|
||||||
|
}
|
||||||
|
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 <any>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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user