diff --git a/lib/doodstream-upload.js b/lib/doodstream-upload.js
index aa908ac..010ad9d 100644
--- a/lib/doodstream-upload.js
+++ b/lib/doodstream-upload.js
@@ -227,16 +227,29 @@ class DoodstreamUploader {
bodyStream.push(preambleBuffer);
bytesSent += preambleBuffer.length;
- // Pipe file
+ // Pipe file with throttle support
fileStream.on('data', (chunk) => {
if (signal && signal.aborted) {
fileStream.destroy();
bodyStream.destroy();
return;
}
- bodyStream.push(chunk);
- bytesSent += chunk.length;
- if (progressCb) progressCb(Math.max(0, bytesSent - preambleBuffer.length), fileSize);
+ if (throttle) {
+ fileStream.pause();
+ throttle.consume(chunk.length, signal).then(() => {
+ bodyStream.push(chunk);
+ bytesSent += chunk.length;
+ if (progressCb) progressCb(Math.max(0, bytesSent - preambleBuffer.length), fileSize);
+ fileStream.resume();
+ }).catch(() => {
+ fileStream.destroy();
+ bodyStream.destroy();
+ });
+ } else {
+ bodyStream.push(chunk);
+ bytesSent += chunk.length;
+ if (progressCb) progressCb(Math.max(0, bytesSent - preambleBuffer.length), fileSize);
+ }
});
fileStream.on('end', () => {
@@ -262,10 +275,16 @@ class DoodstreamUploader {
headersTimeout: 60000
});
+ const statusCode = uploadRes.statusCode;
const resText = await uploadRes.body.text();
let payload;
try { payload = JSON.parse(resText); } catch {}
+ if (statusCode >= 400) {
+ const msg = payload && payload.msg ? payload.msg : resText.slice(0, 200);
+ throw new Error(`Doodstream Upload HTTP ${statusCode}: ${msg}`);
+ }
+
if (!payload) {
// Try to extract from HTML response
const match = resText.match(/filecode['":\s]+['"]([a-zA-Z0-9]+)['"]/i);
@@ -276,7 +295,11 @@ class DoodstreamUploader {
file_code: match[1]
};
}
- throw new Error('Doodstream Upload: Keine gueltige Antwort erhalten');
+ throw new Error(`Doodstream Upload: Keine gueltige Antwort (HTTP ${statusCode}, Body: ${resText.slice(0, 150)})`);
+ }
+
+ if (payload.status && Number(payload.status) !== 200 && payload.msg) {
+ throw new Error(`Doodstream Upload: ${payload.msg}`);
}
// Parse result
@@ -289,7 +312,7 @@ class DoodstreamUploader {
}
if (!item) {
- throw new Error(`Doodstream Upload fehlgeschlagen: ${payload.msg || 'Unbekannter Fehler'}`);
+ throw new Error(`Doodstream Upload fehlgeschlagen: ${payload.msg || JSON.stringify(payload).slice(0, 150)}`);
}
const fileCode = item.filecode || item.file_code || '';
diff --git a/renderer/app.js b/renderer/app.js
index 9ec9698..6aa481f 100644
--- a/renderer/app.js
+++ b/renderer/app.js
@@ -310,10 +310,10 @@ function restoreQueueStateFromConfig() {
}
function buildPersistedQueueState() {
- const unfinishedJobs = queueJobs.filter(job => !['done', 'skipped', 'aborted'].includes(job.status));
+ const persistableJobs = queueJobs.filter(job => !['done', 'skipped'].includes(job.status));
const selectedFileMap = new Map(selectedFiles.map(file => [file.path, file]));
- for (const job of unfinishedJobs) {
+ for (const job of persistableJobs) {
if (job.file && !selectedFileMap.has(job.file)) {
selectedFileMap.set(job.file, {
path: job.file,
@@ -323,7 +323,7 @@ function buildPersistedQueueState() {
}
}
- if (selectedFileMap.size === 0 && queueJobs.every(job => ['done', 'skipped', 'aborted'].includes(job.status))) {
+ if (selectedFileMap.size === 0 && queueJobs.every(job => ['done', 'skipped'].includes(job.status))) {
return null;
}
@@ -335,9 +335,10 @@ function buildPersistedQueueState() {
file: job.file,
fileName: job.fileName,
hoster: job.hoster,
- status: job.status,
+ // Save aborted jobs as queued so they survive restart
+ status: job.status === 'aborted' ? 'queued' : job.status,
bytesTotal: job.bytesTotal || 0,
- error: job.error || null,
+ error: job.status === 'aborted' ? null : (job.error || null),
result: job.result || null,
maxAttempts: job.maxAttempts || 0
}))
@@ -998,6 +999,7 @@ function handleProgress(data) {
scheduleQueueRender();
updateQueueActionButtons();
updateStatusBar();
+ updateStatsPanel();
persistQueueStateSoon();
}
@@ -1035,7 +1037,7 @@ function handleBatchDone(summary) {
renderQueueTable();
}
- if (queueJobs.some((job) => !['done', 'skipped', 'aborted'].includes(job.status))) persistQueueStateSoon(true);
+ if (queueJobs.some((job) => !['done', 'skipped'].includes(job.status))) persistQueueStateSoon(true);
else clearPersistedQueueStateSoon();
lastUploadStats = { state: 'idle', globalSpeedKbs: 0, totalBytes: lastUploadStats.totalBytes, elapsed: lastUploadStats.elapsed, activeJobs: 0 };
@@ -1051,6 +1053,21 @@ function handleStats(data) {
activeJobs: data.activeJobs || 0
};
updateStatusBar();
+ updateStatsPanel();
+
+ // Track run time
+ if (data.state === 'uploading' || data.state === 'stopping') {
+ if (!statsStartTime) {
+ statsStartTime = Date.now();
+ statsRunTimer = setInterval(() => {
+ const el = document.getElementById('statRunTime');
+ if (el) el.textContent = formatDuration(Math.round((Date.now() - statsStartTime) / 1000));
+ }, 1000);
+ }
+ } else if (data.state === 'idle' && statsRunTimer) {
+ clearInterval(statsRunTimer);
+ statsRunTimer = null;
+ }
}
// --- Retry ---
@@ -2220,5 +2237,68 @@ function showCopyToast(msg) {
}
}
+// --- Recent panel tabs ---
+document.querySelectorAll('.recent-tab').forEach(tab => {
+ tab.addEventListener('click', () => {
+ document.querySelectorAll('.recent-tab').forEach(t => t.classList.remove('active'));
+ document.querySelectorAll('.recent-tab-body').forEach(b => b.classList.remove('active'));
+ tab.classList.add('active');
+ const panel = document.getElementById(tab.dataset.panel);
+ if (panel) panel.classList.add('active');
+ const hint = document.getElementById('recentFilesHint');
+ if (hint) hint.textContent = tab.dataset.panel === 'statsTab' ? 'Upload-Statistiken' : 'Zuletzt erzeugte Upload-Links';
+ });
+});
+
+// --- Stats panel update ---
+let statsStartTime = 0;
+let statsRunTimer = null;
+
+function formatBytes(bytes) {
+ if (bytes <= 0) return '0 B';
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
+ return (bytes / Math.pow(1024, i)).toFixed(i > 0 ? 2 : 0) + ' ' + units[i];
+}
+
+function formatDuration(seconds) {
+ const h = Math.floor(seconds / 3600);
+ const m = Math.floor((seconds % 3600) / 60);
+ const s = seconds % 60;
+ return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
+}
+
+function updateStatsPanel() {
+ const total = queueJobs.length;
+ const done = queueJobs.filter(j => j.status === 'done').length;
+ const inProgress = queueJobs.filter(j => ['uploading', 'getting-server', 'retrying'].includes(j.status)).length;
+ const errors = queueJobs.filter(j => j.status === 'error').length;
+ const remaining = total - done - errors;
+
+ const totalSize = queueJobs.reduce((s, j) => s + (j.bytesTotal || 0), 0);
+ const remainingSize = queueJobs.filter(j => !['done', 'error', 'skipped'].includes(j.status))
+ .reduce((s, j) => s + ((j.bytesTotal || 0) - (j.bytesUploaded || 0)), 0);
+
+ const el = (id) => document.getElementById(id);
+ if (el('statQueueTotal')) el('statQueueTotal').textContent = total;
+ if (el('statQueueDone')) el('statQueueDone').textContent = done;
+ if (el('statQueueRemaining')) el('statQueueRemaining').textContent = remaining;
+ if (el('statQueueInProgress')) el('statQueueInProgress').textContent = inProgress;
+ if (el('statQueueError')) el('statQueueError').textContent = errors;
+ if (el('statSizeTotal')) el('statSizeTotal').textContent = formatBytes(totalSize);
+ if (el('statSizeRemaining')) el('statSizeRemaining').textContent = formatBytes(remainingSize);
+
+ const speed = lastUploadStats.globalSpeedKbs || 0;
+ if (el('statSpeed')) el('statSpeed').textContent = speed > 0 ? formatBytes(speed * 1024) + '/s' : '0 B/s';
+ if (el('statEta')) {
+ if (speed > 0 && remainingSize > 0) {
+ el('statEta').textContent = formatDuration(Math.round(remainingSize / (speed * 1024)));
+ } else {
+ el('statEta').textContent = '--:--';
+ }
+ }
+ if (el('statSessionBytes')) el('statSessionBytes').textContent = formatBytes(lastUploadStats.totalBytes || 0);
+}
+
// --- Start ---
init();
diff --git a/renderer/index.html b/renderer/index.html
index e97cdf3..1c0531c 100644
--- a/renderer/index.html
+++ b/renderer/index.html
@@ -96,21 +96,50 @@
-
-
-
-
- | Datum |
- Dateiname |
- Host |
- Link |
-
-
-
-
+
+
+
+
+
+ | Datum |
+ Dateiname |
+ Host |
+ Link |
+
+
+
+
+
+
+
+
+
+
Files in queue (count)
+
total:0
+
done:0
+
remaining:0
+
in progress:0
+
error:0
+
+
+
File size in queue
+
total:0 B
+
remaining:0 B
+
+
+
Session
+
Upload speed:0 B/s
+
Remaining time:--:--
+
Run time:00:00:00
+
Uploaded (this run):0 B
+
+
diff --git a/renderer/styles.css b/renderer/styles.css
index e4c9885..764f5ca 100644
--- a/renderer/styles.css
+++ b/renderer/styles.css
@@ -374,14 +374,67 @@ body {
border-bottom: 1px solid var(--border);
background: rgba(0, 0, 0, 0.12);
}
-.recent-files-header h3 {
- font-size: 13px;
- font-weight: 600;
+.recent-tabs {
+ display: flex;
+ gap: 0;
}
+.recent-tab {
+ padding: 4px 14px;
+ font-size: 12px;
+ font-weight: 500;
+ background: transparent;
+ border: 1px solid var(--border);
+ border-bottom: none;
+ color: var(--text-muted);
+ cursor: pointer;
+ transition: all 0.15s;
+}
+.recent-tab:first-child { border-radius: 4px 0 0 0; }
+.recent-tab:last-child { border-radius: 0 4px 0 0; }
+.recent-tab.active {
+ background: rgba(255,255,255,0.06);
+ color: var(--text);
+ border-bottom-color: transparent;
+}
+.recent-tab:hover:not(.active) {
+ background: rgba(255,255,255,0.03);
+ color: var(--text);
+}
+.recent-tab-body { display: none; flex: 1; min-height: 0; overflow: auto; }
+.recent-tab-body.active { display: flex; flex-direction: column; }
.recent-files-hint {
font-size: 11px;
color: var(--text-dim);
}
+.stats-grid {
+ display: flex;
+ gap: 32px;
+ padding: 12px 20px;
+ flex: 1;
+}
+.stats-col {
+ flex: 1;
+ min-width: 0;
+}
+.stats-col h4 {
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--text-muted);
+ margin-bottom: 6px;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+.stats-row {
+ display: flex;
+ justify-content: space-between;
+ font-size: 12px;
+ padding: 2px 0;
+ color: var(--text-dim);
+}
+.stats-row span:last-child {
+ color: var(--text);
+ font-variant-numeric: tabular-nums;
+}
.recent-files-table-wrap {
flex: 1;
min-height: 0;