Files
metube/ui/src/app/app.component.ts

472 lines
16 KiB
TypeScript
Raw Normal View History

2019-12-03 22:32:07 +02:00
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
2025-01-24 18:40:58 +08:00
import { HttpClient } from '@angular/common/http';
import { faTrashAlt, faCheckCircle, faTimesCircle, IconDefinition } from '@fortawesome/free-regular-svg-icons';
2025-06-02 22:32:23 +03:00
import { faRedoAlt, faSun, faMoon, faCircleHalfStroke, faCheck, faExternalLinkAlt, faDownload, faFileImport, faFileExport, faCopy } from '@fortawesome/free-solid-svg-icons';
2025-06-05 20:47:49 +03:00
import { faGithub } from '@fortawesome/free-brands-svg-icons';
2021-09-13 20:25:32 +03:00
import { CookieService } from 'ngx-cookie-service';
import { map, Observable, of, distinctUntilChanged } from 'rxjs';
2019-11-29 19:31:34 +02:00
import { Download, DownloadsService, Status } from './downloads.service';
2019-12-03 22:32:07 +02:00
import { MasterCheckboxComponent } from './master-checkbox.component';
import { Formats, Format, Quality } from './formats';
import { Theme, Themes } from './theme';
import {KeyValue} from "@angular/common";
2019-11-29 19:31:34 +02:00
@Component({
2025-06-02 23:16:32 +03:00
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.sass'],
standalone: false
2019-11-29 19:31:34 +02:00
})
2019-12-03 22:32:07 +02:00
export class AppComponent implements AfterViewInit {
2019-11-29 19:31:34 +02:00
addUrl: string;
formats: Format[] = Formats;
2021-10-28 11:19:17 +01:00
qualities: Quality[];
quality: string;
2021-09-13 20:25:32 +03:00
format: string;
folder: string;
2023-04-09 11:27:41 +08:00
customNamePrefix: string;
2023-12-09 12:35:31 +08:00
autoStart: boolean;
playlistStrictMode: boolean;
playlistItemLimit: number;
2019-11-29 19:31:34 +02:00
addInProgress = false;
themes: Theme[] = Themes;
activeTheme: Theme;
customDirs$: Observable<string[]>;
showBatchPanel: boolean = false;
batchImportModalOpen = false;
batchImportText = '';
batchImportStatus = '';
importInProgress = false;
cancelImportFlag = false;
2025-06-05 20:47:49 +03:00
ytDlpVersion: string | null = null;
metubeVersion: string | null = null;
2025-06-02 22:32:23 +03:00
isAdvancedOpen = false;
2021-07-29 11:12:40 +03:00
@ViewChild('queueMasterCheckbox') queueMasterCheckbox: MasterCheckboxComponent;
@ViewChild('queueDelSelected') queueDelSelected: ElementRef;
@ViewChild('queueDownloadSelected') queueDownloadSelected: ElementRef;
@ViewChild('doneMasterCheckbox') doneMasterCheckbox: MasterCheckboxComponent;
@ViewChild('doneDelSelected') doneDelSelected: ElementRef;
@ViewChild('doneClearCompleted') doneClearCompleted: ElementRef;
@ViewChild('doneClearFailed') doneClearFailed: ElementRef;
2024-01-26 20:13:34 +05:30
@ViewChild('doneRetryFailed') doneRetryFailed: ElementRef;
@ViewChild('doneDownloadSelected') doneDownloadSelected: ElementRef;
2024-01-26 20:13:34 +05:30
2019-11-29 19:31:34 +02:00
faTrashAlt = faTrashAlt;
2019-12-03 22:32:07 +02:00
faCheckCircle = faCheckCircle;
faTimesCircle = faTimesCircle;
2021-07-29 11:12:40 +03:00
faRedoAlt = faRedoAlt;
2021-12-17 19:30:20 +02:00
faSun = faSun;
faMoon = faMoon;
faCheck = faCheck;
faCircleHalfStroke = faCircleHalfStroke;
faDownload = faDownload;
2022-01-04 22:04:53 +00:00
faExternalLinkAlt = faExternalLinkAlt;
2025-06-02 22:32:23 +03:00
faFileImport = faFileImport;
faFileExport = faFileExport;
faCopy = faCopy;
2025-06-05 20:47:49 +03:00
faGithub = faGithub;
2019-11-29 19:31:34 +02:00
2025-01-24 18:40:58 +08:00
constructor(public downloads: DownloadsService, private cookieService: CookieService, private http: HttpClient) {
2021-09-13 20:25:32 +03:00
this.format = cookieService.get('metube_format') || 'any';
2021-10-28 11:19:17 +01:00
// Needs to be set or qualities won't automatically be set
this.setQualities()
this.quality = cookieService.get('metube_quality') || 'best';
2023-12-10 22:58:45 +02:00
this.autoStart = cookieService.get('metube_auto_start') !== 'false';
2023-12-09 12:35:31 +08:00
this.activeTheme = this.getPreferredTheme(cookieService);
2019-12-03 22:32:07 +02:00
}
ngOnInit() {
this.getConfiguration();
this.customDirs$ = this.getMatchingCustomDir();
this.setTheme(this.activeTheme);
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
if (this.activeTheme.id === 'auto') {
this.setTheme(this.activeTheme);
}
});
}
2022-08-29 21:52:54 -04:00
2019-12-03 22:32:07 +02:00
ngAfterViewInit() {
this.downloads.queueChanged.subscribe(() => {
this.queueMasterCheckbox.selectionChanged();
});
this.downloads.doneChanged.subscribe(() => {
this.doneMasterCheckbox.selectionChanged();
let completed: number = 0, failed: number = 0;
this.downloads.done.forEach(dl => {
if (dl.status === 'finished')
completed++;
else if (dl.status === 'error')
failed++;
});
this.doneClearCompleted.nativeElement.disabled = completed === 0;
this.doneClearFailed.nativeElement.disabled = failed === 0;
2024-01-26 20:13:34 +05:30
this.doneRetryFailed.nativeElement.disabled = failed === 0;
2019-12-03 22:32:07 +02:00
});
2025-01-24 18:40:58 +08:00
this.fetchVersionInfo();
2019-11-29 19:31:34 +02:00
}
// workaround to allow fetching of Map values in the order they were inserted
// https://github.com/angular/angular/issues/31420
asIsOrder(a, b) {
return 1;
}
2021-09-13 20:25:32 +03:00
qualityChanged() {
this.cookieService.set('metube_quality', this.quality, { expires: 3650 });
// Re-trigger custom directory change
this.downloads.customDirsChanged.next(this.downloads.customDirs);
2021-09-13 20:25:32 +03:00
}
showAdvanced() {
return this.downloads.configuration['CUSTOM_DIRS'];
}
allowCustomDir(tag: string) {
if (this.downloads.configuration['CREATE_CUSTOM_DIRS']) {
return tag;
}
return false;
}
isAudioType() {
2024-06-07 10:35:04 +08:00
return this.quality == 'audio' || this.format == 'mp3' || this.format == 'm4a' || this.format == 'opus' || this.format == 'wav' || this.format == 'flac';
}
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"];
}
}),
distinctUntilChanged((prev, curr) => JSON.stringify(prev) === JSON.stringify(curr))
);
2021-09-13 20:25:32 +03:00
}
getConfiguration() {
this.downloads.configurationChanged.subscribe({
next: (config) => {
this.playlistStrictMode = config['DEFAULT_OPTION_PLAYLIST_STRICT_MODE'];
const playlistItemLimit = config['DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT'];
if (playlistItemLimit !== '0') {
this.playlistItemLimit = playlistItemLimit;
}
}
});
}
getPreferredTheme(cookieService: CookieService) {
let theme = 'auto';
if (cookieService.check('metube_theme')) {
theme = cookieService.get('metube_theme');
2021-12-16 21:52:56 +00:00
}
return this.themes.find(x => x.id === theme) ?? this.themes.find(x => x.id === 'auto');
2021-12-16 21:52:56 +00:00
}
themeChanged(theme: Theme) {
this.cookieService.set('metube_theme', theme.id, { expires: 3650 });
this.setTheme(theme);
2021-12-16 21:52:56 +00:00
}
setTheme(theme: Theme) {
this.activeTheme = theme;
if (theme.id === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-bs-theme', 'dark');
} else {
document.documentElement.setAttribute('data-bs-theme', theme.id);
}
2021-12-16 21:52:56 +00:00
}
2021-09-13 20:25:32 +03:00
formatChanged() {
this.cookieService.set('metube_format', this.format, { expires: 3650 });
2021-10-28 11:19:17 +01:00
// Updates to use qualities available
this.setQualities()
// Re-trigger custom directory change
this.downloads.customDirsChanged.next(this.downloads.customDirs);
2021-09-13 20:25:32 +03:00
}
2023-12-09 12:35:31 +08:00
autoStartChanged() {
this.cookieService.set('metube_auto_start', this.autoStart ? 'true' : 'false', { expires: 3650 });
}
2019-12-03 22:32:07 +02:00
queueSelectionChanged(checked: number) {
this.queueDelSelected.nativeElement.disabled = checked == 0;
this.queueDownloadSelected.nativeElement.disabled = checked == 0;
2019-11-29 19:31:34 +02:00
}
2019-12-03 22:32:07 +02:00
doneSelectionChanged(checked: number) {
this.doneDelSelected.nativeElement.disabled = checked == 0;
this.doneDownloadSelected.nativeElement.disabled = checked == 0;
2019-11-29 19:31:34 +02:00
}
2021-10-28 11:19:17 +01:00
setQualities() {
// qualities for specific format
this.qualities = this.formats.find(el => el.id == this.format).qualities
const exists = this.qualities.find(el => el.id === this.quality)
this.quality = exists ? this.quality : 'best'
2021-10-28 11:19:17 +01:00
}
addDownload(url?: string, quality?: string, format?: string, folder?: string, customNamePrefix?: string, playlistStrictMode?: boolean, playlistItemLimit?: number, autoStart?: boolean) {
2021-07-29 11:12:40 +03:00
url = url ?? this.addUrl
quality = quality ?? this.quality
format = format ?? this.format
folder = folder ?? this.folder
2023-04-09 11:27:41 +08:00
customNamePrefix = customNamePrefix ?? this.customNamePrefix
playlistStrictMode = playlistStrictMode ?? this.playlistStrictMode
playlistItemLimit = playlistItemLimit ?? this.playlistItemLimit
2023-12-09 12:35:31 +08:00
autoStart = autoStart ?? this.autoStart
2021-07-29 11:12:40 +03:00
console.debug('Downloading: url='+url+' quality='+quality+' format='+format+' folder='+folder+' customNamePrefix='+customNamePrefix+' playlistStrictMode='+playlistStrictMode+' playlistItemLimit='+playlistItemLimit+' autoStart='+autoStart);
2019-11-29 19:31:34 +02:00
this.addInProgress = true;
this.downloads.add(url, quality, format, folder, customNamePrefix, playlistStrictMode, playlistItemLimit, autoStart).subscribe((status: Status) => {
2019-11-29 19:31:34 +02:00
if (status.status === 'error') {
alert(`Error adding URL: ${status.msg}`);
} else {
this.addUrl = '';
}
this.addInProgress = false;
});
}
2023-12-09 12:35:31 +08:00
downloadItemByKey(id: string) {
this.downloads.startById([id]).subscribe();
}
retryDownload(key: string, download: Download) {
this.addDownload(download.url, download.quality, download.format, download.folder, download.custom_name_prefix, download.playlist_strict_mode, download.playlist_item_limit, true);
this.downloads.delById('done', [key]).subscribe();
2021-07-29 11:12:40 +03:00
}
2019-12-03 22:32:07 +02:00
delDownload(where: string, id: string) {
this.downloads.delById(where, [id]).subscribe();
}
startSelectedDownloads(where: string){
this.downloads.startByFilter(where, dl => dl.checked).subscribe();
}
2019-12-03 22:32:07 +02:00
delSelectedDownloads(where: string) {
this.downloads.delByFilter(where, dl => dl.checked).subscribe();
}
clearCompletedDownloads() {
this.downloads.delByFilter('done', dl => dl.status === 'finished').subscribe();
2019-11-29 19:31:34 +02:00
}
2019-12-03 22:32:07 +02:00
clearFailedDownloads() {
this.downloads.delByFilter('done', dl => dl.status === 'error').subscribe();
2019-11-29 19:31:34 +02:00
}
2024-01-26 20:13:34 +05:30
retryFailedDownloads() {
this.downloads.done.forEach((dl, key) => {
if (dl.status === 'error') {
this.retryDownload(key, dl);
}
});
}
downloadSelectedFiles() {
this.downloads.done.forEach((dl, key) => {
if (dl.status === 'finished' && dl.checked) {
const link = document.createElement('a');
link.href = this.buildDownloadLink(dl);
link.setAttribute('download', dl.filename);
link.setAttribute('target', '_self');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
});
}
buildDownloadLink(download: Download) {
2024-05-02 21:31:44 +03:00
let baseDir = this.downloads.configuration["PUBLIC_HOST_URL"];
if (download.quality == 'audio' || download.filename.endsWith('.mp3')) {
2024-05-02 13:50:00 +02:00
baseDir = this.downloads.configuration["PUBLIC_HOST_AUDIO_URL"];
}
if (download.folder) {
baseDir += download.folder + '/';
}
return baseDir + encodeURIComponent(download.filename);
}
identifyDownloadRow(index: number, row: KeyValue<string, Download>) {
return row.key;
}
isNumber(event) {
const charCode = (event.which) ? event.which : event.keyCode;
if (charCode > 31 && (charCode < 48 || charCode > 57)) {
event.preventDefault();
}
}
// Toggle inline batch panel (if you want to use an inline panel for export; not used for import modal)
toggleBatchPanel(): void {
this.showBatchPanel = !this.showBatchPanel;
}
// Open the Batch Import modal
openBatchImportModal(): void {
this.batchImportModalOpen = true;
this.batchImportText = '';
this.batchImportStatus = '';
this.importInProgress = false;
this.cancelImportFlag = false;
}
// Close the Batch Import modal
closeBatchImportModal(): void {
this.batchImportModalOpen = false;
}
// Start importing URLs from the batch modal textarea
startBatchImport(): void {
const urls = this.batchImportText
.split(/\r?\n/)
.map(url => url.trim())
.filter(url => url.length > 0);
if (urls.length === 0) {
alert('No valid URLs found.');
return;
}
this.importInProgress = true;
this.cancelImportFlag = false;
this.batchImportStatus = `Starting to import ${urls.length} URLs...`;
let index = 0;
const delayBetween = 1000;
const processNext = () => {
if (this.cancelImportFlag) {
this.batchImportStatus = `Import cancelled after ${index} of ${urls.length} URLs.`;
this.importInProgress = false;
return;
}
if (index >= urls.length) {
this.batchImportStatus = `Finished importing ${urls.length} URLs.`;
this.importInProgress = false;
return;
}
const url = urls[index];
this.batchImportStatus = `Importing URL ${index + 1} of ${urls.length}: ${url}`;
2025-03-25 11:08:10 -07:00
// Now pass the selected quality, format, folder, etc. to the add() method
this.downloads.add(url, this.quality, this.format, this.folder, this.customNamePrefix,
this.playlistStrictMode, this.playlistItemLimit, this.autoStart)
.subscribe({
next: (status: Status) => {
if (status.status === 'error') {
alert(`Error adding URL ${url}: ${status.msg}`);
}
index++;
setTimeout(processNext, delayBetween);
},
error: (err) => {
console.error(`Error importing URL ${url}:`, err);
index++;
setTimeout(processNext, delayBetween);
}
});
};
processNext();
}
// Cancel the batch import process
cancelBatchImport(): void {
if (this.importInProgress) {
this.cancelImportFlag = true;
this.batchImportStatus += ' Cancelling...';
}
}
2025-03-05 19:12:29 +02:00
// Export URLs based on filter: 'pending', 'completed', 'failed', or 'all'
exportBatchUrls(filter: 'pending' | 'completed' | 'failed' | 'all'): void {
let urls: string[];
if (filter === 'pending') {
urls = Array.from(this.downloads.queue.values()).map(dl => dl.url);
} else if (filter === 'completed') {
// Only finished downloads in the "done" Map
urls = Array.from(this.downloads.done.values()).filter(dl => dl.status === 'finished').map(dl => dl.url);
} else if (filter === 'failed') {
// Only error downloads from the "done" Map
urls = Array.from(this.downloads.done.values()).filter(dl => dl.status === 'error').map(dl => dl.url);
} else {
// All: pending + both finished and error in done
urls = [
...Array.from(this.downloads.queue.values()).map(dl => dl.url),
...Array.from(this.downloads.done.values()).map(dl => dl.url)
];
}
if (!urls.length) {
alert('No URLs found for the selected filter.');
return;
}
const content = urls.join('\n');
const blob = new Blob([content], { type: 'text/plain' });
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = 'metube_urls.txt';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(downloadUrl);
}
// Copy URLs to clipboard based on filter: 'pending', 'completed', 'failed', or 'all'
copyBatchUrls(filter: 'pending' | 'completed' | 'failed' | 'all'): void {
let urls: string[];
if (filter === 'pending') {
urls = Array.from(this.downloads.queue.values()).map(dl => dl.url);
} else if (filter === 'completed') {
urls = Array.from(this.downloads.done.values()).filter(dl => dl.status === 'finished').map(dl => dl.url);
} else if (filter === 'failed') {
urls = Array.from(this.downloads.done.values()).filter(dl => dl.status === 'error').map(dl => dl.url);
} else {
urls = [
...Array.from(this.downloads.queue.values()).map(dl => dl.url),
...Array.from(this.downloads.done.values()).map(dl => dl.url)
];
}
if (!urls.length) {
alert('No URLs found for the selected filter.');
return;
}
const content = urls.join('\n');
navigator.clipboard.writeText(content)
.then(() => alert('URLs copied to clipboard.'))
.catch(() => alert('Failed to copy URLs.'));
}
2025-01-24 18:40:58 +08:00
fetchVersionInfo(): void {
const baseUrl = `${window.location.origin}${window.location.pathname.replace(/\/[^\/]*$/, '/')}`;
const versionUrl = `${baseUrl}version`;
2025-06-05 20:47:49 +03:00
this.http.get<{ 'yt-dlp': string, version: string }>(versionUrl)
2025-01-24 18:40:58 +08:00
.subscribe({
next: (data) => {
2025-06-05 20:47:49 +03:00
this.ytDlpVersion = data['yt-dlp'];
this.metubeVersion = data.version;
2025-01-24 18:40:58 +08:00
},
error: () => {
2025-06-05 20:47:49 +03:00
this.ytDlpVersion = null;
this.metubeVersion = null;
2025-01-24 18:40:58 +08:00
}
});
}
2025-06-02 22:32:23 +03:00
toggleAdvanced() {
this.isAdvancedOpen = !this.isAdvancedOpen;
}
2019-11-29 19:31:34 +02:00
}