Merge remote-tracking branch 'origin/master' into download_to_device
This commit is contained in:
@@ -26,7 +26,7 @@
|
||||
<div class="container add-url-box">
|
||||
<div class="row">
|
||||
<div class="col add-url-component input-group">
|
||||
<input type="text" class="form-control" placeholder="Video or playlist URL" name="addUrl" [(ngModel)]="addUrl" [disabled]="addInProgress || downloads.loading">
|
||||
<input type="text" autocomplete="off" spellcheck="false" class="form-control" placeholder="Video or playlist URL" name="addUrl" [(ngModel)]="addUrl" [disabled]="addInProgress || downloads.loading">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -47,10 +47,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 add-url-component">
|
||||
<button class="btn btn-primary add-url" type="submit" (click)="addDownload()" [disabled]="addInProgress || downloads.loading">
|
||||
<span class="spinner-border spinner-border-sm" role="status" id="add-spinner" *ngIf="addInProgress"></span>
|
||||
{{ addInProgress ? "Adding..." : "Add" }}
|
||||
</button>
|
||||
<div [attr.class]="showAdvanced() ? 'btn-group add-url-group' : 'add-url-group'" ngbDropdown #advancedDropdown="ngbDropdown" display="dynamic" placement="bottom-end">
|
||||
<button class="btn btn-primary add-url" type="submit" (click)="addDownload()" [disabled]="addInProgress || downloads.loading">
|
||||
<span class="spinner-border spinner-border-sm" role="status" id="add-spinner" *ngIf="addInProgress"></span>
|
||||
{{ addInProgress ? "Adding..." : "Add" }}
|
||||
</button>
|
||||
<button class="btn btn-primary dropdown-toggle dropdown-toggle-split" id="advancedButton" type="button" title="Advanced options" [disabled]="addInProgress || downloads.loading" ngbDropdownAnchor (focus)="advancedDropdown.open()" *ngIf="showAdvanced()">
|
||||
<span class="sr-only">Advanced options</span>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="advancedButton" class="dropdown-menu dropdown-menu-end folder-dropdown-menu">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Download Folder</span>
|
||||
<ng-select [items]="customDirs$ | async" placeholder="Default" [addTag]="allowCustomDir.bind(this)" addTagText="Create directory" [ngStyle]="{'flex-grow':'1'}" bindLabel="folder" [(ngModel)]="folder" [disabled]="addInProgress || downloads.loading"></ng-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,11 +130,11 @@
|
||||
<fa-icon *ngIf="download.value.status == 'finished'" [icon]="faCheckCircle" style="color: green;"></fa-icon>
|
||||
<fa-icon *ngIf="download.value.status == 'error'" [icon]="faTimesCircle" style="color: red;"></fa-icon>
|
||||
</div>
|
||||
<span ngbTooltip="{{download.value.msg}}"><a *ngIf="!!download.value.filename; else noDownloadLink" href="download/{{download.value.filename | encodeURIComponent}}" target="_blank">{{ download.value.title }}</a></span>
|
||||
<span ngbTooltip="{{download.value.msg}}"><a *ngIf="!!download.value.filename; else noDownloadLink" href="{{buildDownloadLink(download.value)}}" target="_blank">{{ download.value.title }}</a></span>
|
||||
<ng-template #noDownloadLink>{{ download.value.title }}</ng-template>
|
||||
</td>
|
||||
<td>
|
||||
<button *ngIf="download.value.status == 'error'" type="button" class="btn btn-link" (click)="retryDownload(download.key, download.value.url, download.value.quality)"><fa-icon [icon]="faRedoAlt"></fa-icon></button>
|
||||
<button *ngIf="download.value.status == 'error'" type="button" class="btn btn-link" (click)="retryDownload(download.key, download.value.url, download.value.quality, download.value.folder)"><fa-icon [icon]="faRedoAlt"></fa-icon></button>
|
||||
</td>
|
||||
<td>
|
||||
<a *ngIf="!!download.value.filename; else noDownloadLink" href="download/{{download.value.filename | encodeURIComponent}}" download target="_blank"><fa-icon [icon]="faDownload"></fa-icon></a>
|
||||
|
||||
@@ -9,9 +9,20 @@
|
||||
.add-url-component
|
||||
margin: 0.5rem auto
|
||||
|
||||
.add-url-group
|
||||
width: 100%
|
||||
|
||||
button.add-url
|
||||
width: 100%
|
||||
|
||||
.folder-dropdown-menu
|
||||
width: 500px
|
||||
|
||||
.folder-dropdown-menu .input-group
|
||||
display: flex
|
||||
padding-left: 5px
|
||||
padding-right: 5px
|
||||
|
||||
$metube-section-color-bg: rgba(0,0,0,.07)
|
||||
|
||||
.metube-section-header
|
||||
|
||||
@@ -2,15 +2,16 @@ import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
|
||||
import { faTrashAlt, faCheckCircle, faTimesCircle } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faRedoAlt, faSun, faMoon, faExternalLinkAlt, faDownload } from '@fortawesome/free-solid-svg-icons';
|
||||
import { CookieService } from 'ngx-cookie-service';
|
||||
import { map, Observable, of } from 'rxjs';
|
||||
|
||||
import { DownloadsService, Status } from './downloads.service';
|
||||
import { Download, DownloadsService, Status } from './downloads.service';
|
||||
import { MasterCheckboxComponent } from './master-checkbox.component';
|
||||
import { Formats, Format, Quality } from './formats';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.sass']
|
||||
styleUrls: ['./app.component.sass'],
|
||||
})
|
||||
export class AppComponent implements AfterViewInit {
|
||||
addUrl: string;
|
||||
@@ -18,8 +19,10 @@ export class AppComponent implements AfterViewInit {
|
||||
qualities: Quality[];
|
||||
quality: string;
|
||||
format: string;
|
||||
folder: string;
|
||||
addInProgress = false;
|
||||
darkMode: boolean;
|
||||
customDirs$: Observable<string[]>;
|
||||
|
||||
@ViewChild('queueMasterCheckbox') queueMasterCheckbox: MasterCheckboxComponent;
|
||||
@ViewChild('queueDelSelected') queueDelSelected: ElementRef;
|
||||
@@ -45,6 +48,10 @@ export class AppComponent implements AfterViewInit {
|
||||
this.setupTheme(cookieService)
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.customDirs$ = this.getMatchingCustomDir();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.downloads.queueChanged.subscribe(() => {
|
||||
this.queueMasterCheckbox.selectionChanged();
|
||||
@@ -71,6 +78,36 @@ export class AppComponent implements AfterViewInit {
|
||||
|
||||
qualityChanged() {
|
||||
this.cookieService.set('metube_quality', this.quality, { expires: 3650 });
|
||||
// Re-trigger custom directory change
|
||||
this.downloads.customDirsChanged.next(this.downloads.customDirs);
|
||||
}
|
||||
|
||||
showAdvanced() {
|
||||
return this.downloads.configuration['CUSTOM_DIRS'];
|
||||
}
|
||||
|
||||
allowCustomDir(tag: string) {
|
||||
if (this.downloads.configuration['CREATE_CUSTOM_DIRS']) {
|
||||
return tag;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
isAudioType() {
|
||||
return this.quality == 'audio' || this.format == 'mp3';
|
||||
}
|
||||
|
||||
getMatchingCustomDir() : Observable<string[]> {
|
||||
return this.downloads.customDirsChanged.asObservable().pipe(map((output) => {
|
||||
// Keep logic consistent with app/ytdl.py
|
||||
if (this.isAudioType()) {
|
||||
console.debug("Showing audio-specific download directories");
|
||||
return output["audio_download_dir"];
|
||||
} else {
|
||||
console.debug("Showing default download directories");
|
||||
return output["download_dir"];
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
setupTheme(cookieService) {
|
||||
@@ -98,6 +135,8 @@ export class AppComponent implements AfterViewInit {
|
||||
this.cookieService.set('metube_format', this.format, { expires: 3650 });
|
||||
// Updates to use qualities available
|
||||
this.setQualities()
|
||||
// Re-trigger custom directory change
|
||||
this.downloads.customDirsChanged.next(this.downloads.customDirs);
|
||||
}
|
||||
|
||||
queueSelectionChanged(checked: number) {
|
||||
@@ -115,13 +154,15 @@ export class AppComponent implements AfterViewInit {
|
||||
this.quality = exists ? this.quality : 'best'
|
||||
}
|
||||
|
||||
addDownload(url?: string, quality?: string, format?: string) {
|
||||
addDownload(url?: string, quality?: string, format?: string, folder?: string) {
|
||||
url = url ?? this.addUrl
|
||||
quality = quality ?? this.quality
|
||||
format = format ?? this.format
|
||||
folder = folder ?? this.folder
|
||||
|
||||
console.debug('Downloading: url='+url+' quality='+quality+' format='+format+' folder='+folder);
|
||||
this.addInProgress = true;
|
||||
this.downloads.add(url, quality, format).subscribe((status: Status) => {
|
||||
this.downloads.add(url, quality, format, folder).subscribe((status: Status) => {
|
||||
if (status.status === 'error') {
|
||||
alert(`Error adding URL: ${status.msg}`);
|
||||
} else {
|
||||
@@ -131,8 +172,8 @@ export class AppComponent implements AfterViewInit {
|
||||
});
|
||||
}
|
||||
|
||||
retryDownload(key: string, url: string, quality: string, format: string) {
|
||||
this.addDownload(url, quality, format);
|
||||
retryDownload(key: string, url: string, quality: string, format: string, folder: string) {
|
||||
this.addDownload(url, quality, format, folder);
|
||||
this.downloads.delById('done', [key]).subscribe();
|
||||
}
|
||||
|
||||
@@ -151,4 +192,17 @@ export class AppComponent implements AfterViewInit {
|
||||
clearFailedDownloads() {
|
||||
this.downloads.delByFilter('done', dl => dl.status === 'error').subscribe();
|
||||
}
|
||||
|
||||
buildDownloadLink(download: Download) {
|
||||
let baseDir = 'download/';
|
||||
if (download.quality == 'audio' || download.filename.endsWith('.mp3')) {
|
||||
baseDir = 'audio_download/';
|
||||
}
|
||||
|
||||
if (download.folder) {
|
||||
baseDir += download.folder + '/';
|
||||
}
|
||||
|
||||
return baseDir + encodeURIComponent(download.filename);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AppComponent } from './app.component';
|
||||
import { EtaPipe, SpeedPipe, EncodeURIComponent } from './downloads.pipe';
|
||||
import { MasterCheckboxComponent, SlaveCheckboxComponent } from './master-checkbox.component';
|
||||
import { MeTubeSocket } from './metube-socket';
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -25,7 +26,8 @@ import { MeTubeSocket } from './metube-socket';
|
||||
FormsModule,
|
||||
NgbModule,
|
||||
HttpClientModule,
|
||||
FontAwesomeModule
|
||||
FontAwesomeModule,
|
||||
NgSelectModule
|
||||
],
|
||||
providers: [CookieService, MeTubeSocket],
|
||||
bootstrap: [AppComponent]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
import { of, Subject } from 'rxjs';
|
||||
import { Observable, of, Subject } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { MeTubeSocket } from './metube-socket';
|
||||
|
||||
@@ -9,13 +9,14 @@ export interface Status {
|
||||
msg?: string;
|
||||
}
|
||||
|
||||
interface Download {
|
||||
export interface Download {
|
||||
id: string;
|
||||
title: string;
|
||||
url: string,
|
||||
status: string;
|
||||
msg: string;
|
||||
filename: string;
|
||||
folder: string;
|
||||
quality: string;
|
||||
percent: number;
|
||||
speed: number;
|
||||
@@ -33,6 +34,10 @@ export class DownloadsService {
|
||||
done = new Map<string, Download>();
|
||||
queueChanged = new Subject();
|
||||
doneChanged = new Subject();
|
||||
customDirsChanged = new Subject();
|
||||
|
||||
configuration = {};
|
||||
customDirs = {};
|
||||
|
||||
constructor(private http: HttpClient, private socket: MeTubeSocket) {
|
||||
socket.fromEvent('all').subscribe((strdata: string) => {
|
||||
@@ -47,20 +52,20 @@ export class DownloadsService {
|
||||
});
|
||||
socket.fromEvent('added').subscribe((strdata: string) => {
|
||||
let data: Download = JSON.parse(strdata);
|
||||
this.queue.set(data.id, data);
|
||||
this.queue.set(data.url, data);
|
||||
this.queueChanged.next(null);
|
||||
});
|
||||
socket.fromEvent('updated').subscribe((strdata: string) => {
|
||||
let data: Download = JSON.parse(strdata);
|
||||
let dl: Download = this.queue.get(data.id);
|
||||
let dl: Download = this.queue.get(data.url);
|
||||
data.checked = dl.checked;
|
||||
data.deleting = dl.deleting;
|
||||
this.queue.set(data.id, data);
|
||||
this.queue.set(data.url, data);
|
||||
});
|
||||
socket.fromEvent('completed').subscribe((strdata: string) => {
|
||||
let data: Download = JSON.parse(strdata);
|
||||
this.queue.delete(data.id);
|
||||
this.done.set(data.id, data);
|
||||
this.queue.delete(data.url);
|
||||
this.done.set(data.url, data);
|
||||
this.queueChanged.next(null);
|
||||
this.doneChanged.next(null);
|
||||
});
|
||||
@@ -74,6 +79,17 @@ export class DownloadsService {
|
||||
this.done.delete(data);
|
||||
this.doneChanged.next(null);
|
||||
});
|
||||
socket.fromEvent('configuration').subscribe((strdata: string) => {
|
||||
let data = JSON.parse(strdata);
|
||||
console.debug("got configuration:", data);
|
||||
this.configuration = data;
|
||||
});
|
||||
socket.fromEvent('custom_dirs').subscribe((strdata: string) => {
|
||||
let data = JSON.parse(strdata);
|
||||
console.debug("got custom_dirs:", data);
|
||||
this.customDirs = data;
|
||||
this.customDirsChanged.next(data);
|
||||
});
|
||||
}
|
||||
|
||||
handleHTTPError(error: HttpErrorResponse) {
|
||||
@@ -81,8 +97,8 @@ export class DownloadsService {
|
||||
return of({status: 'error', msg: msg})
|
||||
}
|
||||
|
||||
public add(url: string, quality: string, format: string) {
|
||||
return this.http.post<Status>('add', {url: url, quality: quality, format: format}).pipe(
|
||||
public add(url: string, quality: string, format: string, folder: string) {
|
||||
return this.http.post<Status>('add', {url: url, quality: quality, format: format, folder: folder}).pipe(
|
||||
catchError(this.handleHTTPError)
|
||||
);
|
||||
}
|
||||
@@ -94,7 +110,7 @@ export class DownloadsService {
|
||||
|
||||
public delByFilter(where: string, filter: (dl: Download) => boolean) {
|
||||
let ids: string[] = [];
|
||||
this[where].forEach((dl: Download) => { if (filter(dl)) ids.push(dl.id) });
|
||||
this[where].forEach((dl: Download) => { if (filter(dl)) ids.push(dl.url) });
|
||||
return this.delById(where, ids);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,4 +43,11 @@ export const Formats: Format[] = [
|
||||
{ id: '128', text: '128 kbps' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'thumbnail',
|
||||
text: 'Thumbnail',
|
||||
qualities: [
|
||||
{ id: 'best', text: 'Best' }
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
|
||||
/* Importing Bootstrap SCSS file. */
|
||||
@import '~bootstrap/scss/bootstrap'
|
||||
@import '~@ng-select/ng-select/themes/default.theme.css'
|
||||
Reference in New Issue
Block a user