614 lines
20 KiB
TypeScript
614 lines
20 KiB
TypeScript
import { Component, OnInit } from '@angular/core';
|
|
import { HttpClient } from '@angular/common/http';
|
|
import { Observable, distinctUntilChanged, map } from 'rxjs';
|
|
|
|
import { Download, DownloadsService, Status } from './downloads.service';
|
|
import { Formats, Format, Quality } from './formats';
|
|
import { Theme, Themes } from './theme';
|
|
import { PreferencesService } from './preferences.service';
|
|
import { ToastService } from './toast.service';
|
|
import { KeyedDownload } from './active-downloads.component';
|
|
|
|
export type DownloadTab = 'downloading' | 'queued' | 'completed';
|
|
|
|
@Component({
|
|
selector: 'app-root',
|
|
templateUrl: './app.component.html',
|
|
styleUrls: ['./app.component.sass'],
|
|
standalone: false
|
|
})
|
|
export class AppComponent implements OnInit {
|
|
formats: Format[] = Formats;
|
|
qualities: Quality[];
|
|
themes: Theme[] = Themes;
|
|
|
|
// Add-form state
|
|
addUrl = '';
|
|
quality: string;
|
|
format: string;
|
|
folder = '';
|
|
customNamePrefix = '';
|
|
autoStart: boolean;
|
|
playlistStrictMode = false;
|
|
playlistItemLimit: number | null = null;
|
|
addInProgress = false;
|
|
customDirs$: Observable<string[]>;
|
|
|
|
// Navigation & list state
|
|
status: DownloadTab = 'queued';
|
|
selectedFolder = 'all';
|
|
filter = '';
|
|
sort: 'date' | 'title' | 'size' = 'date';
|
|
sortAscending = false;
|
|
view: 'list' | 'grid' = 'list';
|
|
isAdvancedOpen = false;
|
|
sidebarOpen = false;
|
|
activityOpen = false;
|
|
|
|
// Metrics
|
|
activeDownloads = 0;
|
|
queuedDownloads = 0;
|
|
completedDownloads = 0;
|
|
failedDownloads = 0;
|
|
totalSpeed = 0;
|
|
librarySize = 0;
|
|
|
|
// Diagnostics / events
|
|
ytDlpOptionsUpdateTime: string | null = null;
|
|
ytDlpVersion: string | null = null;
|
|
metubeVersion: string | null = null;
|
|
events: Array<{ type: string; message: string; timestamp: number; url: string }> = [];
|
|
|
|
// Batch import modal
|
|
batchImportModalOpen = false;
|
|
batchImportText = '';
|
|
batchImportStatus = '';
|
|
importInProgress = false;
|
|
cancelImportFlag = false;
|
|
|
|
activeTheme: Theme;
|
|
|
|
constructor(
|
|
public downloads: DownloadsService,
|
|
private preferences: PreferencesService,
|
|
private toastService: ToastService,
|
|
private http: HttpClient
|
|
) {
|
|
this.format = this.preferences.get('metube_format', 'any');
|
|
this.setQualities();
|
|
this.quality = this.preferences.get('metube_quality', 'best');
|
|
this.autoStart = this.preferences.getBool('metube_auto_start', true);
|
|
this.sortAscending = this.preferences.getBool('metube_sort_order', false);
|
|
this.folder = this.preferences.get('metube_folder', '');
|
|
const savedStatus = this.preferences.get('metube_status', 'queued');
|
|
this.status = savedStatus === 'downloading' || savedStatus === 'completed' ? savedStatus : 'queued';
|
|
const savedFolder = this.preferences.get('metube_folder_nav', 'all');
|
|
this.selectedFolder = savedFolder;
|
|
const savedView = this.preferences.get('metube_view', 'list');
|
|
this.view = savedView === 'grid' ? 'grid' : 'list';
|
|
this.activeTheme = this.getPreferredTheme();
|
|
|
|
this.downloads.queueChanged.subscribe(() => this.updateMetrics());
|
|
this.downloads.doneChanged.subscribe(() => this.updateMetrics());
|
|
this.downloads.updated.subscribe(() => this.updateMetrics());
|
|
this.downloads.eventReceived.subscribe((event: any) => {
|
|
this.events.push(event);
|
|
if (this.events.length > 5) this.events = this.events.slice(-5);
|
|
});
|
|
}
|
|
|
|
ngOnInit() {
|
|
this.getConfiguration();
|
|
this.getYtdlOptionsUpdateTime();
|
|
this.customDirs$ = this.getMatchingCustomDir();
|
|
this.setTheme(this.activeTheme);
|
|
this.loadEvents();
|
|
this.fetchVersionInfo();
|
|
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
|
if (this.activeTheme.id === 'auto') this.setTheme(this.activeTheme);
|
|
});
|
|
}
|
|
|
|
// ---------- Navigation ----------
|
|
|
|
onStatusChange(status: DownloadTab): void {
|
|
this.status = status;
|
|
this.preferences.set('metube_status', status);
|
|
}
|
|
|
|
onFolderChange(folder: string): void {
|
|
this.selectedFolder = folder;
|
|
this.preferences.set('metube_folder_nav', folder);
|
|
}
|
|
|
|
setSortDirection(ascending: boolean): void {
|
|
this.sortAscending = ascending;
|
|
this.preferences.setBool('metube_sort_order', ascending);
|
|
}
|
|
|
|
setView(view: 'list' | 'grid'): void {
|
|
this.view = view;
|
|
this.preferences.set('metube_view', view);
|
|
}
|
|
|
|
toggleAdvanced(open?: boolean): void {
|
|
this.isAdvancedOpen = open === undefined ? !this.isAdvancedOpen : open;
|
|
}
|
|
|
|
// ---------- Data getters ----------
|
|
|
|
statusEntries(tab: DownloadTab): KeyedDownload[] {
|
|
if (tab === 'completed') {
|
|
return Array.from(this.downloads.done.entries()).map(([key, value]) => ({ key, value }));
|
|
}
|
|
const entries: KeyedDownload[] = [];
|
|
this.downloads.queue.forEach((dl, key) => {
|
|
if (tab === 'queued' ? dl.status === 'pending' : dl.status !== 'pending') {
|
|
entries.push({ key, value: dl });
|
|
}
|
|
});
|
|
return entries;
|
|
}
|
|
|
|
get downloadingRows(): KeyedDownload[] {
|
|
return this.statusEntries('downloading').sort((a, b) => (a.value.timestamp || 0) - (b.value.timestamp || 0));
|
|
}
|
|
|
|
get filteredRows(): KeyedDownload[] {
|
|
let rows = this.statusEntries(this.status);
|
|
if (this.selectedFolder !== 'all') {
|
|
rows = rows.filter(r => (r.value.folder || '') === this.selectedFolder);
|
|
}
|
|
const q = this.filter.trim().toLowerCase();
|
|
if (q) {
|
|
rows = rows.filter(r => r.value.title && r.value.title.toLowerCase().includes(q));
|
|
}
|
|
const dir = this.sortAscending ? 1 : -1;
|
|
if (this.sort === 'title') {
|
|
rows.sort((a, b) => a.value.title.localeCompare(b.value.title, 'zh') * dir);
|
|
} else if (this.sort === 'size') {
|
|
rows.sort((a, b) => ((a.value.size || 0) - (b.value.size || 0)) * dir);
|
|
} else {
|
|
rows.sort((a, b) => ((a.value.timestamp || 0) - (b.value.timestamp || 0)) * dir);
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
get statusCounts(): { downloading: number; queued: number; completed: number } {
|
|
return {
|
|
downloading: this.activeDownloads,
|
|
queued: this.queuedDownloads,
|
|
completed: this.downloads.done.size
|
|
};
|
|
}
|
|
|
|
get folderNavs(): { name: string; count: number }[] {
|
|
const counts = new Map<string, number>();
|
|
for (const { value } of this.statusEntries(this.status)) {
|
|
const key = value.folder || '';
|
|
counts.set(key, (counts.get(key) || 0) + 1);
|
|
}
|
|
return Array.from(counts.entries())
|
|
.map(([name, count]) => ({ name, count }))
|
|
.sort((a, b) => a.name.localeCompare(b.name, 'zh'));
|
|
}
|
|
|
|
get allCount(): number {
|
|
return this.statusEntries(this.status).length;
|
|
}
|
|
|
|
get folderName(): string {
|
|
return this.selectedFolder === 'all' ? '全部文件夹' : (this.selectedFolder || '未分类');
|
|
}
|
|
|
|
// ---------- Add / download actions ----------
|
|
|
|
setQuality(quality: string): void {
|
|
this.quality = quality;
|
|
this.preferences.set('metube_quality', quality);
|
|
this.downloads.customDirsChanged.next(this.downloads.customDirs);
|
|
}
|
|
|
|
setFormat(format: string): void {
|
|
this.format = format;
|
|
this.preferences.set('metube_format', format);
|
|
this.setQualities();
|
|
this.downloads.customDirsChanged.next(this.downloads.customDirs);
|
|
}
|
|
|
|
setFolder(folder: string | null): void {
|
|
this.folder = folder || '';
|
|
this.preferences.set('metube_folder', this.folder);
|
|
}
|
|
|
|
setAutoStart(autoStart: boolean): void {
|
|
this.autoStart = autoStart;
|
|
this.preferences.setBool('metube_auto_start', autoStart);
|
|
}
|
|
|
|
addDownload(url?: string, quality?: string, format?: string, folder?: string, customNamePrefix?: string, playlistStrictMode?: boolean, playlistItemLimit?: number, autoStart?: boolean) {
|
|
url = url ?? this.addUrl;
|
|
quality = quality ?? this.quality;
|
|
format = format ?? this.format;
|
|
folder = folder ?? this.folder;
|
|
customNamePrefix = customNamePrefix ?? this.customNamePrefix;
|
|
playlistStrictMode = playlistStrictMode ?? this.playlistStrictMode;
|
|
playlistItemLimit = playlistItemLimit ?? this.playlistItemLimit;
|
|
autoStart = autoStart ?? this.autoStart;
|
|
|
|
if (!url || !/^https?:\/\//i.test(url.trim())) {
|
|
this.toastService.show('请先粘贴一个有效的视频或播放列表 URL', { error: true });
|
|
return;
|
|
}
|
|
|
|
this.addInProgress = true;
|
|
this.downloads.add(url.trim(), quality, format, folder, customNamePrefix, playlistStrictMode, playlistItemLimit, autoStart).subscribe((status: Status) => {
|
|
this.addInProgress = false;
|
|
if (status.status === 'error') {
|
|
this.toastService.show(`添加失败: ${status.msg}`, { error: true });
|
|
} else {
|
|
this.addUrl = '';
|
|
this.toastService.show('已加入队列');
|
|
this.loadEvents();
|
|
}
|
|
});
|
|
}
|
|
|
|
startItems(ids: string[]): void {
|
|
if (!ids.length) return;
|
|
this.downloads.startById(ids).subscribe(() => {
|
|
this.toastService.show(`已开始 ${ids.length} 个任务`);
|
|
});
|
|
}
|
|
|
|
delItems(ids: string[], where: 'queue' | 'done' = 'done'): void {
|
|
if (!ids.length) return;
|
|
this.downloads.delById(where, ids).subscribe(() => {
|
|
this.toastService.show(`已删除 ${ids.length} 项`);
|
|
});
|
|
}
|
|
|
|
cancelDownload(key: string): void {
|
|
this.downloads.delById('queue', [key]).subscribe(() => {
|
|
this.toastService.show('已取消下载');
|
|
});
|
|
}
|
|
|
|
moveItems(ids: string[], folder: string): void {
|
|
if (!ids.length) return;
|
|
this.downloads.moveById(ids, folder).subscribe((status: Status) => {
|
|
if (status.status === 'error') {
|
|
this.toastService.show(`移动失败: ${status.msg}`, { error: true });
|
|
} else {
|
|
this.toastService.show(`已将 ${ids.length} 项移动到 ${folder || '默认目录'}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
folderChanged(key: string, folder: string): void {
|
|
this.downloads.moveById([key], folder).subscribe((status: Status) => {
|
|
if (status.status === 'error') {
|
|
this.toastService.show(`更改目录失败: ${status.msg}`, { error: true });
|
|
}
|
|
});
|
|
}
|
|
|
|
retryDownload(key: string, download: Download): void {
|
|
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();
|
|
}
|
|
|
|
clearCompletedDownloads(): void {
|
|
this.downloads.delByFilter('done', dl => dl.status === 'finished').subscribe();
|
|
}
|
|
|
|
clearFailedDownloads(): void {
|
|
this.downloads.delByFilter('done', dl => dl.status === 'error').subscribe();
|
|
}
|
|
|
|
retryFailedDownloads(): void {
|
|
this.downloads.done.forEach((dl, key) => {
|
|
if (dl.status === 'error') this.retryDownload(key, dl);
|
|
});
|
|
}
|
|
|
|
downloadSelectedFiles(ids: string[]): void {
|
|
let count = 0;
|
|
ids.forEach(key => {
|
|
const dl = this.downloads.done.get(key);
|
|
if (dl && dl.status === 'finished' && dl.filename) {
|
|
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);
|
|
count++;
|
|
}
|
|
});
|
|
if (count === 0) {
|
|
this.toastService.show('所选任务没有可下载的文件', { error: true });
|
|
}
|
|
}
|
|
|
|
buildDownloadLink(download: Download): string {
|
|
let baseDir = this.downloads.configuration['PUBLIC_HOST_URL'];
|
|
if (download.quality === 'audio' || (download.filename && download.filename.endsWith('.mp3'))) {
|
|
baseDir = this.downloads.configuration['PUBLIC_HOST_AUDIO_URL'];
|
|
}
|
|
if (download.folder) {
|
|
baseDir += download.folder + '/';
|
|
}
|
|
return baseDir + encodeURIComponent(download.filename);
|
|
}
|
|
|
|
// ---------- Batch import / export / cookies ----------
|
|
|
|
openBatchImportModal(): void {
|
|
this.batchImportModalOpen = true;
|
|
this.batchImportText = '';
|
|
this.batchImportStatus = '';
|
|
this.importInProgress = false;
|
|
this.cancelImportFlag = false;
|
|
}
|
|
|
|
closeBatchImportModal(): void {
|
|
this.batchImportModalOpen = false;
|
|
}
|
|
|
|
startBatchImport(): void {
|
|
const urls = this.batchImportText.split(/\r?\n/).map(u => u.trim()).filter(u => u.length > 0);
|
|
if (urls.length === 0) {
|
|
this.toastService.show('没有找到有效的 URL', { error: true });
|
|
return;
|
|
}
|
|
this.importInProgress = true;
|
|
this.cancelImportFlag = false;
|
|
this.batchImportStatus = `开始导入 ${urls.length} 个 URL…`;
|
|
let index = 0;
|
|
const delayBetween = 1000;
|
|
const processNext = () => {
|
|
if (this.cancelImportFlag) {
|
|
this.batchImportStatus = `已取消导入(完成 ${index}/${urls.length})`;
|
|
this.importInProgress = false;
|
|
return;
|
|
}
|
|
if (index >= urls.length) {
|
|
this.batchImportStatus = `已完成 ${urls.length} 个 URL 的导入`;
|
|
this.importInProgress = false;
|
|
this.toastService.show(`导入完成,共 ${urls.length} 条`);
|
|
return;
|
|
}
|
|
const url = urls[index];
|
|
this.batchImportStatus = `正在导入 ${index + 1}/${urls.length}: ${url}`;
|
|
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') {
|
|
this.toastService.show(`导入失败 ${url}: ${status.msg}`, { error: true });
|
|
}
|
|
index++;
|
|
setTimeout(processNext, delayBetween);
|
|
},
|
|
error: (err) => {
|
|
console.error(`Error importing URL ${url}:`, err);
|
|
index++;
|
|
setTimeout(processNext, delayBetween);
|
|
}
|
|
});
|
|
};
|
|
processNext();
|
|
}
|
|
|
|
cancelBatchImport(): void {
|
|
if (this.importInProgress) {
|
|
this.cancelImportFlag = true;
|
|
this.batchImportStatus += ' 正在取消…';
|
|
}
|
|
}
|
|
|
|
private collectUrls(filter: 'pending' | 'completed' | 'failed' | 'all'): string[] {
|
|
if (filter === 'pending') {
|
|
return Array.from(this.downloads.queue.values()).map(dl => dl.url);
|
|
}
|
|
if (filter === 'completed') {
|
|
return Array.from(this.downloads.done.values()).filter(dl => dl.status === 'finished').map(dl => dl.url);
|
|
}
|
|
if (filter === 'failed') {
|
|
return Array.from(this.downloads.done.values()).filter(dl => dl.status === 'error').map(dl => dl.url);
|
|
}
|
|
return [
|
|
...Array.from(this.downloads.queue.values()).map(dl => dl.url),
|
|
...Array.from(this.downloads.done.values()).map(dl => dl.url)
|
|
];
|
|
}
|
|
|
|
exportBatchUrls(filter: 'pending' | 'completed' | 'failed' | 'all'): void {
|
|
const urls = this.collectUrls(filter);
|
|
if (!urls.length) {
|
|
this.toastService.show('没有可导出的 URL', { error: true });
|
|
return;
|
|
}
|
|
const blob = new Blob([urls.join('\n')], { 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);
|
|
this.toastService.show(`已导出 ${urls.length} 条 URL`);
|
|
}
|
|
|
|
copyBatchUrls(filter: 'pending' | 'completed' | 'failed' | 'all'): void {
|
|
const urls = this.collectUrls(filter);
|
|
if (!urls.length) {
|
|
this.toastService.show('没有可复制的 URL', { error: true });
|
|
return;
|
|
}
|
|
navigator.clipboard.writeText(urls.join('\n'))
|
|
.then(() => this.toastService.show(`已复制 ${urls.length} 条 URL`))
|
|
.catch(() => this.toastService.show('复制失败', { error: true }));
|
|
}
|
|
|
|
saveCookie(domainOrUrl: string, cookie: string): void {
|
|
const isUrl = /^https?:\/\//i.test(domainOrUrl);
|
|
const url = isUrl ? domainOrUrl : '';
|
|
const domain = isUrl ? '' : domainOrUrl.replace(/^www\./i, '');
|
|
this.downloads.setCookie(cookie, domain, url).subscribe((status: any) => {
|
|
if (status.status === 'error') {
|
|
this.toastService.show(`Cookie 保存失败: ${status.msg}`, { error: true });
|
|
} else {
|
|
this.toastService.show(`Cookie 已保存(${status.cookie_count || ''} 条)`);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ---------- Configuration / theme / diagnostics ----------
|
|
|
|
allowCustomDir(tag: string) {
|
|
return this.downloads.configuration['CREATE_CUSTOM_DIRS'] ? tag : false;
|
|
}
|
|
|
|
isAudioType(): boolean {
|
|
return this.quality === 'audio' || ['mp3', 'm4a', 'opus', 'wav', 'flac'].includes(this.format);
|
|
}
|
|
|
|
getMatchingCustomDir(): Observable<string[]> {
|
|
return this.downloads.customDirsChanged.asObservable().pipe(
|
|
map((output: any) => {
|
|
if (this.isAudioType()) {
|
|
return output['audio_download_dir'];
|
|
}
|
|
return output['download_dir'];
|
|
}),
|
|
distinctUntilChanged((prev, curr) => JSON.stringify(prev) === JSON.stringify(curr))
|
|
);
|
|
}
|
|
|
|
customDirsForDownload(dl: Download): string[] {
|
|
const isAudio = dl.quality === 'audio' || ['m4a', 'mp3', 'opus', 'wav', 'flac'].includes(dl.format);
|
|
const dirs = this.downloads.customDirs[isAudio ? 'audio_download_dir' : 'download_dir'];
|
|
return dirs || [];
|
|
}
|
|
|
|
setQualities(): void {
|
|
const format = this.formats.find(el => el.id === this.format) || this.formats[0];
|
|
this.qualities = format.qualities;
|
|
const exists = this.qualities.find(el => el.id === this.quality);
|
|
this.quality = exists ? this.quality : 'best';
|
|
}
|
|
|
|
getPreferredTheme(): Theme {
|
|
const id = this.preferences.get('metube_theme', 'auto');
|
|
return this.themes.find(x => x.id === id) ?? this.themes.find(x => x.id === 'auto');
|
|
}
|
|
|
|
themeChanged(theme: Theme): void {
|
|
this.preferences.set('metube_theme', theme.id);
|
|
this.setTheme(theme);
|
|
}
|
|
|
|
setTheme(theme: Theme): void {
|
|
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);
|
|
}
|
|
}
|
|
|
|
getConfiguration(): void {
|
|
this.downloads.configurationChanged.subscribe({
|
|
next: (config: any) => {
|
|
this.playlistStrictMode = config['DEFAULT_OPTION_PLAYLIST_STRICT_MODE'];
|
|
const limit = config['DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT'];
|
|
if (limit !== '0') this.playlistItemLimit = limit;
|
|
}
|
|
});
|
|
}
|
|
|
|
getYtdlOptionsUpdateTime(): void {
|
|
this.downloads.ytdlOptionsChanged.subscribe({
|
|
next: (data: any) => {
|
|
if (data['success']) {
|
|
this.ytDlpOptionsUpdateTime = new Date(data['update_time'] * 1000).toLocaleString();
|
|
} else {
|
|
this.toastService.show(`加载 yt-dlp 选项失败: ${data['msg']}`, { error: true });
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fetchVersionInfo(): void {
|
|
const baseUrl = `${window.location.origin}${window.location.pathname.replace(/\/[^\/]*$/, '/')}`;
|
|
this.http.get<{ 'yt-dlp': string; version: string }>(`${baseUrl}version`)
|
|
.subscribe({
|
|
next: (data) => {
|
|
this.ytDlpVersion = data['yt-dlp'];
|
|
this.metubeVersion = data.version;
|
|
},
|
|
error: () => {
|
|
this.ytDlpVersion = null;
|
|
this.metubeVersion = null;
|
|
}
|
|
});
|
|
}
|
|
|
|
loadEvents(): void {
|
|
this.http.get('/events').subscribe((events: any[]) => {
|
|
this.events = events || [];
|
|
});
|
|
}
|
|
|
|
clearEvents(): void {
|
|
this.http.post('/events/clear', {}).subscribe(() => {
|
|
this.events = [];
|
|
});
|
|
}
|
|
|
|
getRelativeTime(timestamp: number): string {
|
|
const now = Date.now() / 1000;
|
|
const diff = Math.floor(now - timestamp);
|
|
if (diff < 60) return '刚刚';
|
|
if (diff < 3600) return `${Math.floor(diff / 60)} 分钟前`;
|
|
if (diff < 86400) return `${Math.floor(diff / 3600)} 小时前`;
|
|
return `${Math.floor(diff / 86400)} 天前`;
|
|
}
|
|
|
|
private updateMetrics(): void {
|
|
let active = 0;
|
|
let queued = 0;
|
|
let speed = 0;
|
|
this.downloads.queue.forEach(dl => {
|
|
if (dl.status === 'downloading' || dl.status === 'preparing') {
|
|
active++;
|
|
speed += dl.speed || 0;
|
|
} else if (dl.status === 'pending') {
|
|
queued++;
|
|
}
|
|
});
|
|
this.activeDownloads = active;
|
|
this.queuedDownloads = queued;
|
|
this.totalSpeed = speed;
|
|
|
|
let completed = 0;
|
|
let failed = 0;
|
|
let library = 0;
|
|
this.downloads.done.forEach(dl => {
|
|
if (dl.status === 'finished') {
|
|
completed++;
|
|
library += dl.size || 0;
|
|
} else if (dl.status === 'error') {
|
|
failed++;
|
|
}
|
|
});
|
|
this.completedDownloads = completed;
|
|
this.failedDownloads = failed;
|
|
this.librarySize = library;
|
|
}
|
|
}
|