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
pull/11/head
Isaac Grynsztein 6 years ago
parent d54b2a73c8
commit 5f4a7a1e69

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

@ -7,11 +7,32 @@
<mat-card-content>
<div style="position: relative;">
<form class="example-form">
<mat-form-field class="example-full-width">
<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>
<button class="input-clear-button" mat-icon-button (click)="clearInput()"><mat-icon>clear</mat-icon></button>
</mat-form-field>
<div class="container-fluid">
<div class="row">
<div class="col-12 col-sm-9">
<mat-form-field class="example-full-width">
<input style="padding-right: 25px;" matInput (ngModelChange)="inputChanged($event)" [(ngModel)]="url" [placeholder]="'URL' + (youtubeSearchEnabled ? ' or search' : '')" type="url" name="url" [formControl]="urlForm" required #urlinput>
<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>
</div>
<div class="col-7 col-sm-3">
<mat-form-field style="display: inline-block; width: inherit; min-width: 120px;">
<mat-label>Quality</mat-label>
<mat-select [ngModelOptions]="{standalone: true}" [(ngModel)]="selectedQuality">
<ng-container *ngFor="let option of qualityOptions[(audioOnly) ? 'audio' : 'video']">
<mat-option *ngIf="option.value === '' || url && cachedAvailableFormats[url] && cachedAvailableFormats[url] && cachedAvailableFormats[url][(audioOnly) ? 'audio' : 'video'][option.value]" [value]="option.value">
{{option.label}}
</mat-option>
</ng-container>
</mat-select>
<div class="spinner-div" *ngIf="formats_loading && !cachedAvailableFormats[url]">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
</mat-form-field>
</div>
</div>
</div>
<span *ngIf="results_showing">
<span *ngFor="let result of results">
<mat-card style="height: 120px; border-radius: 0px">
@ -29,7 +50,7 @@
</span>
</form>
<br/>
<mat-checkbox [(ngModel)]="audioOnly" style="float: left; margin-top: -12px">Only Audio</mat-checkbox>
<mat-checkbox (change)="videoModeChanged($event)" [(ngModel)]="audioOnly" style="float: left; margin-top: -12px">Only Audio</mat-checkbox>
</div>
</mat-card-content>
@ -41,16 +62,18 @@
</div>
<br/>
<div class="centered big" id="bar_div" *ngIf="downloadingfile;else nofile">
<div [ngClass]="(determinateProgress && percentDownloaded === 100)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="determinateProgress;else indeterminateprogress">
<mat-progress-bar mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
<br/>
<div class="margined">
<div [ngClass]="(determinateProgress && percentDownloaded === 100)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="determinateProgress;else indeterminateprogress">
<mat-progress-bar mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
<br/>
</div>
<div *ngIf="determinateProgress && percentDownloaded === 100" class="spinner">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
<ng-template #indeterminateprogress>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</ng-template>
</div>
<div *ngIf="determinateProgress && percentDownloaded === 100" class="spinner">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
<ng-template #indeterminateprogress>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</ng-template>
<br/>
</div>
<ng-template #nofile>

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

Loading…
Cancel
Save