diff options
Diffstat (limited to 'ui/src/app')
-rw-r--r-- | ui/src/app/app.component.html | 29 | ||||
-rw-r--r-- | ui/src/app/app.component.sass | 11 | ||||
-rw-r--r-- | ui/src/app/app.component.ts | 69 | ||||
-rw-r--r-- | ui/src/app/app.module.ts | 4 | ||||
-rw-r--r-- | ui/src/app/downloads.service.ts | 36 | ||||
-rw-r--r-- | ui/src/app/formats.ts | 7 |
6 files changed, 131 insertions, 25 deletions
diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 74425aa..4807064 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -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 (click)="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> @@ -106,6 +117,7 @@ <th scope="col" style="width: 2rem;"></th> <th scope="col" style="width: 2rem;"></th> <th scope="col" style="width: 2rem;"></th> + <th scope="col" style="width: 2rem;"></th> </tr> </thead> <tbody> @@ -118,11 +130,14 @@ <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="{{buildDownloadLink(download.value)}}" download><fa-icon [icon]="faDownload"></fa-icon></a> </td> <td> <a href="{{download.value.url}}" target="_blank"><fa-icon [icon]="faExternalLinkAlt"></fa-icon></a> diff --git a/ui/src/app/app.component.sass b/ui/src/app/app.component.sass index 517af03..0f98556 100644 --- a/ui/src/app/app.component.sass +++ b/ui/src/app/app.component.sass @@ -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
diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index a9b46a3..7038b68 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -1,16 +1,17 @@ import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; import { faTrashAlt, faCheckCircle, faTimesCircle } from '@fortawesome/free-regular-svg-icons'; -import { faRedoAlt, faSun, faMoon, faExternalLinkAlt } from '@fortawesome/free-solid-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; @@ -34,6 +37,7 @@ export class AppComponent implements AfterViewInit { faRedoAlt = faRedoAlt; faSun = faSun; faMoon = faMoon; + faDownload = faDownload; faExternalLinkAlt = faExternalLinkAlt; constructor(public downloads: DownloadsService, private cookieService: CookieService) { @@ -44,6 +48,10 @@ export class AppComponent implements AfterViewInit { this.setupTheme(cookieService) } + ngOnInit() { + this.customDirs$ = this.getMatchingCustomDir(); + } + ngAfterViewInit() { this.downloads.queueChanged.subscribe(() => { this.queueMasterCheckbox.selectionChanged(); @@ -70,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) { @@ -97,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) { @@ -114,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 { @@ -130,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(); } @@ -150,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); + } } diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index 6ad5978..8eddbca 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -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] diff --git a/ui/src/app/downloads.service.ts b/ui/src/app/downloads.service.ts index 8580a70..e1a134d 100644 --- a/ui/src/app/downloads.service.ts +++ b/ui/src/app/downloads.service.ts @@ -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); } } diff --git a/ui/src/app/formats.ts b/ui/src/app/formats.ts index 15be903..a3bf3e8 100644 --- a/ui/src/app/formats.ts +++ b/ui/src/app/formats.ts @@ -43,4 +43,11 @@ export const Formats: Format[] = [ { id: '128', text: '128 kbps' }, ], }, + { + id: 'thumbnail', + text: 'Thumbnail', + qualities: [ + { id: 'best', text: 'Best' } + ], + }, ]; |