mirror of
https://github.com/alexta69/metube.git
synced 2026-03-03 02:47:02 +00:00
Updated ui and backend
Added Sequential, limited and concurrent downloading and import export buttons
This commit is contained in:
@@ -119,6 +119,49 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="d-flex justify-content-end align-items-center my-3">
|
||||
<!-- Batch Import Button (opens modal) -->
|
||||
<button class="btn btn-secondary me-2" (click)="openBatchImportModal()">
|
||||
Batch Import
|
||||
</button>
|
||||
<!-- Batch Export All -->
|
||||
<button class="btn btn-secondary me-2" (click)="exportBatchUrls('all')">
|
||||
Batch Export All
|
||||
</button>
|
||||
<!-- Batch Copy All -->
|
||||
<button class="btn btn-secondary me-2" (click)="copyBatchUrls('all')">
|
||||
Batch Copy All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Batch Import Modal -->
|
||||
<div class="modal fade" tabindex="-1" role="dialog" [ngClass]="{'show': batchImportModalOpen}" [ngStyle]="{'display': batchImportModalOpen ? 'block' : 'none'}">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Batch Import URLs</h5>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="closeBatchImportModal()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<textarea [(ngModel)]="batchImportText" class="form-control" rows="6"
|
||||
placeholder="Paste one video URL per line"></textarea>
|
||||
<div class="mt-2">
|
||||
<small *ngIf="batchImportStatus">{{ batchImportStatus }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger me-auto" *ngIf="importInProgress" (click)="cancelBatchImport()">
|
||||
Cancel Import
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" (click)="closeBatchImportModal()">Close</button>
|
||||
<button type="button" class="btn btn-primary" (click)="startBatchImport()" [disabled]="importInProgress">
|
||||
Import URLs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div *ngIf="downloads.loading" class="alert alert-info" role="alert">
|
||||
Connecting to server...
|
||||
|
||||
@@ -64,4 +64,42 @@ td
|
||||
|
||||
.download-progressbar
|
||||
width: 12rem
|
||||
margin-left: auto
|
||||
margin-left: auto
|
||||
|
||||
.batch-panel
|
||||
margin-top: 15px
|
||||
border: 1px solid #ccc
|
||||
border-radius: 8px
|
||||
padding: 15px
|
||||
background-color: #fff
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1)
|
||||
|
||||
.batch-panel-header
|
||||
border-bottom: 1px solid #eee
|
||||
padding-bottom: 8px
|
||||
margin-bottom: 15px
|
||||
h4
|
||||
font-size: 1.5rem
|
||||
margin: 0
|
||||
|
||||
.batch-panel-body
|
||||
textarea.form-control
|
||||
resize: vertical
|
||||
|
||||
.batch-status
|
||||
font-size: 0.9rem
|
||||
color: #555
|
||||
|
||||
.d-flex.my-3
|
||||
margin-top: 1rem
|
||||
margin-bottom: 1rem
|
||||
|
||||
.modal.fade.show
|
||||
background-color: rgba(0, 0, 0, 0.5)
|
||||
|
||||
.modal-header
|
||||
border-bottom: 1px solid #eee
|
||||
.modal-body
|
||||
textarea.form-control
|
||||
resize: vertical
|
||||
|
||||
|
||||
@@ -30,6 +30,12 @@ export class AppComponent implements AfterViewInit {
|
||||
themes: Theme[] = Themes;
|
||||
activeTheme: Theme;
|
||||
customDirs$: Observable<string[]>;
|
||||
showBatchPanel: boolean = false;
|
||||
batchImportModalOpen = false;
|
||||
batchImportText = '';
|
||||
batchImportStatus = '';
|
||||
importInProgress = false;
|
||||
cancelImportFlag = false;
|
||||
|
||||
@ViewChild('queueMasterCheckbox') queueMasterCheckbox: MasterCheckboxComponent;
|
||||
@ViewChild('queueDelSelected') queueDelSelected: ElementRef;
|
||||
@@ -293,4 +299,133 @@ export class AppComponent implements AfterViewInit {
|
||||
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}`;
|
||||
this.downloads.addDownloadByUrl(url)
|
||||
.then(() => {
|
||||
index++;
|
||||
setTimeout(processNext, delayBetween);
|
||||
})
|
||||
.catch(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...';
|
||||
}
|
||||
}
|
||||
|
||||
// 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.'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -129,4 +129,26 @@ export class DownloadsService {
|
||||
this[where].forEach((dl: Download) => { if (filter(dl)) ids.push(dl.url) });
|
||||
return this.delById(where, ids);
|
||||
}
|
||||
public addDownloadByUrl(url: string): Promise<any> {
|
||||
const defaultQuality = 'best';
|
||||
const defaultFormat = 'mp4';
|
||||
const defaultFolder = '';
|
||||
const defaultCustomNamePrefix = '';
|
||||
const defaultPlaylistStrictMode = false;
|
||||
const defaultPlaylistItemLimit = 0;
|
||||
const defaultAutoStart = true;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.add(url, defaultQuality, defaultFormat, defaultFolder, defaultCustomNamePrefix, defaultPlaylistStrictMode, defaultPlaylistItemLimit, defaultAutoStart)
|
||||
.subscribe(
|
||||
response => resolve(response),
|
||||
error => reject(error)
|
||||
);
|
||||
});
|
||||
}
|
||||
public exportQueueUrls(): string[] {
|
||||
return Array.from(this.queue.values()).map(download => download.url);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user