feat: global speed limit, settings cleanup, abort reset, resize panel
- Global speed throttle (shared across all uploads) - Settings grouped into sections (Uploads, Verhalten, Log) - Abort all resets jobs to queued (restartable without reupload) - fileuploader.log writes immediately per upload - Staggered interval per hoster (not parallel sleep) - Recent files panel resizable via drag handle - History hides aborted entries - Done jobs removed from queue immediately when setting active Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bfe98eac0c
commit
3d858b1ffd
@ -31,6 +31,7 @@ const DEFAULTS = {
|
||||
parallelUploadCount: 0, // 0 = use per-hoster limits only
|
||||
scaleParallelUploads: false,
|
||||
removeFromQueueOnDone: false,
|
||||
globalMaxSpeedKbs: 0, // 0 = unlimited global speed
|
||||
pendingQueue: null,
|
||||
scramble: {
|
||||
active: false,
|
||||
|
||||
@ -34,6 +34,8 @@ class UploadManager extends EventEmitter {
|
||||
this.jobAbortControllers = new Map(); // jobId -> AbortController
|
||||
this.cancelledJobIds = new Set();
|
||||
this.sessionBytes = 0;
|
||||
this.lastStartTime = {}; // hoster -> timestamp of last upload start
|
||||
this.globalThrottle = null;
|
||||
}
|
||||
|
||||
_getSettings(hoster) {
|
||||
@ -62,6 +64,17 @@ class UploadManager extends EventEmitter {
|
||||
return this.globalSemaphore;
|
||||
}
|
||||
|
||||
_getGlobalThrottle() {
|
||||
const kbs = Number(this.globalSettings.globalMaxSpeedKbs || 0);
|
||||
if (!Number.isFinite(kbs) || kbs <= 0) return null;
|
||||
if (!this.globalThrottle) {
|
||||
this.globalThrottle = new Throttle(kbs * 1024);
|
||||
} else {
|
||||
this.globalThrottle.updateRate(kbs * 1024);
|
||||
}
|
||||
return this.globalThrottle;
|
||||
}
|
||||
|
||||
_getSemaphore(hoster) {
|
||||
if (!this.semaphores[hoster]) {
|
||||
const settings = this._getSettings(hoster);
|
||||
@ -83,6 +96,8 @@ class UploadManager extends EventEmitter {
|
||||
this.cancelledJobIds.clear();
|
||||
this.semaphores = {};
|
||||
this.globalSemaphore = null;
|
||||
this.globalThrottle = null;
|
||||
this.lastStartTime = {};
|
||||
|
||||
const { signal } = this.abortController;
|
||||
const batchId = `batch-${Date.now()}`;
|
||||
@ -206,7 +221,7 @@ class UploadManager extends EventEmitter {
|
||||
hosterSlotAcquired = true;
|
||||
|
||||
if (settings.timeIntervalSec > 0) {
|
||||
await this._sleep(settings.timeIntervalSec * 1000, signal);
|
||||
await this._waitForInterval(task.hoster, settings.timeIntervalSec * 1000, signal);
|
||||
}
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
@ -255,9 +270,13 @@ class UploadManager extends EventEmitter {
|
||||
maxAttempts
|
||||
});
|
||||
|
||||
const throttle = settings.maxSpeedKbs > 0
|
||||
const hosterThrottle = settings.maxSpeedKbs > 0
|
||||
? new Throttle(settings.maxSpeedKbs * 1024)
|
||||
: null;
|
||||
const globalThrottle = this._getGlobalThrottle();
|
||||
const throttle = hosterThrottle && globalThrottle
|
||||
? { consume: async (bytes, sig) => { await hosterThrottle.consume(bytes, sig); await globalThrottle.consume(bytes, sig); } }
|
||||
: hosterThrottle || globalThrottle;
|
||||
|
||||
if (settings.restartBelowKbs > 0) {
|
||||
speedAbort = new AbortController();
|
||||
@ -503,6 +522,16 @@ class UploadManager extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
async _waitForInterval(hoster, intervalMs, signal) {
|
||||
const now = Date.now();
|
||||
const last = this.lastStartTime[hoster] || 0;
|
||||
const elapsed = now - last;
|
||||
if (elapsed < intervalMs) {
|
||||
await this._sleep(intervalMs - elapsed, signal);
|
||||
}
|
||||
this.lastStartTime[hoster] = Date.now();
|
||||
}
|
||||
|
||||
cancelJobs(jobIds) {
|
||||
for (const jobId of jobIds || []) {
|
||||
if (!jobId) continue;
|
||||
|
||||
19
main.js
19
main.js
@ -540,6 +540,13 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
||||
if (data.status !== 'uploading') {
|
||||
debugLog(`progress: ${data.fileName} ${data.hoster} ${data.status} ${data.error || ''}`);
|
||||
}
|
||||
// Write to fileuploader.log immediately when a single upload finishes
|
||||
if (data.status === 'done' && data.result) {
|
||||
const link = data.result.download_url || data.result.embed_url || '';
|
||||
if (link) {
|
||||
appendUploadLog(data.hoster || '', link, data.fileName || '');
|
||||
}
|
||||
}
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('upload-progress', data);
|
||||
}
|
||||
@ -554,18 +561,6 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
||||
uploadManager.on('batch-done', async (summary) => {
|
||||
debugLog(`batch-done: total=${summary.total} ok=${summary.succeeded} fail=${summary.failed}`);
|
||||
await configStore.appendHistory(summary);
|
||||
// Write successful uploads to fileuploader.log
|
||||
for (const file of summary.files || []) {
|
||||
for (const result of file.results || []) {
|
||||
if (result.status === 'done' && (result.download_url || result.embed_url)) {
|
||||
appendUploadLog(
|
||||
result.hoster || '',
|
||||
result.download_url || result.embed_url || '',
|
||||
file.name || ''
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('upload-batch-done', summary);
|
||||
}
|
||||
|
||||
116
renderer/app.js
116
renderer/app.js
@ -923,6 +923,19 @@ async function startUpload() {
|
||||
async function cancelUpload() {
|
||||
await window.api.cancelUpload();
|
||||
uploading = false;
|
||||
// Reset all non-finished jobs back to queued state
|
||||
for (const job of queueJobs) {
|
||||
if (!['done', 'error', 'skipped'].includes(job.status)) {
|
||||
job.status = 'queued';
|
||||
job.progress = 0;
|
||||
job.bytesUploaded = 0;
|
||||
job.speedKbs = 0;
|
||||
job.elapsed = 0;
|
||||
job.remaining = 0;
|
||||
job.error = null;
|
||||
}
|
||||
}
|
||||
renderQueueTable();
|
||||
updateQueueActionButtons();
|
||||
updateStatusBar();
|
||||
persistQueueStateSoon();
|
||||
@ -975,6 +988,13 @@ function handleProgress(data) {
|
||||
|
||||
maybeAddSessionFile(job);
|
||||
|
||||
// Remove finished jobs from queue immediately if setting is enabled
|
||||
if (job.status === 'done' && config.globalSettings && config.globalSettings.removeFromQueueOnDone) {
|
||||
removeJobFromIndex(job);
|
||||
selectedJobIds.delete(job.id);
|
||||
queueJobs = queueJobs.filter(j => j !== job);
|
||||
}
|
||||
|
||||
scheduleQueueRender();
|
||||
updateQueueActionButtons();
|
||||
updateStatusBar();
|
||||
@ -984,6 +1004,20 @@ function handleProgress(data) {
|
||||
function handleBatchDone(summary) {
|
||||
uploading = false;
|
||||
applySummaryResults(summary);
|
||||
|
||||
// Reset aborted jobs back to queued so they can be restarted
|
||||
for (const job of queueJobs) {
|
||||
if (job.status === 'aborted') {
|
||||
job.status = 'queued';
|
||||
job.progress = 0;
|
||||
job.bytesUploaded = 0;
|
||||
job.speedKbs = 0;
|
||||
job.elapsed = 0;
|
||||
job.remaining = 0;
|
||||
job.error = null;
|
||||
}
|
||||
}
|
||||
|
||||
syncSelectedFilesFromQueue();
|
||||
updateQueueActionButtons();
|
||||
renderQueueTable();
|
||||
@ -1307,32 +1341,40 @@ function renderSettings() {
|
||||
<span class="panel-status active">System</span>
|
||||
</div>
|
||||
<div class="hoster-panel-body" data-panel="global" style="display:block">
|
||||
<div class="settings-row">
|
||||
<label>FileUploader Log</label>
|
||||
<input type="text" class="key-input settings-autosave" id="logFilePathInput" value="${escapeAttr(globalSettings.logFilePath || '')}" placeholder="Standardpfad verwenden">
|
||||
<button class="btn btn-xs btn-secondary" id="chooseLogFilePathBtn">Ordner wählen</button>
|
||||
</div>
|
||||
<div class="settings-row checkbox-row">
|
||||
<label>Queue-Wiederherstellung beim Start</label>
|
||||
<input type="checkbox" class="settings-autosave" id="resumeQueueOnLaunchInput" ${globalSettings.resumeQueueOnLaunch === false ? '' : 'checked'}>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label></label>
|
||||
<span class="hint">Stellt die Queue beim Start wieder her, startet aber pausiert.</span>
|
||||
</div>
|
||||
<div class="settings-section-label">Uploads</div>
|
||||
<div class="settings-grid-mini">
|
||||
<div class="settings-row">
|
||||
<label>Globale parallele Uploads</label>
|
||||
<input type="number" class="hs-input settings-autosave" id="parallelUploadCountInput" value="${globalSettings.parallelUploadCount ?? 0}" min="0" max="100">
|
||||
<span class="hint">0 = nur pro Hoster</span>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label>Globales Speed-Limit (MB/s)</label>
|
||||
<input type="number" class="hs-input settings-autosave" id="globalMaxSpeedMbsInput" value="${globalSettings.globalMaxSpeedKbs > 0 ? (globalSettings.globalMaxSpeedKbs / 1024).toFixed(2).replace(/\\.00$/, '') : '0'}" min="0" step="0.1">
|
||||
<span class="hint">0 = unbegrenzt</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section-label">Verhalten</div>
|
||||
<div class="settings-grid-mini">
|
||||
<div class="settings-row checkbox-row">
|
||||
<label>Hoster-Limits hochskalieren</label>
|
||||
<input type="checkbox" class="settings-autosave" id="scaleParallelUploadsInput" ${globalSettings.scaleParallelUploads ? 'checked' : ''}>
|
||||
</div>
|
||||
<div class="settings-row checkbox-row">
|
||||
<label>Aus der Queue entfernen bei Abschluss</label>
|
||||
<label>Aus Queue entfernen bei Abschluss</label>
|
||||
<input type="checkbox" class="settings-autosave" id="removeFromQueueOnDoneInput" ${globalSettings.removeFromQueueOnDone ? 'checked' : ''}>
|
||||
</div>
|
||||
<div class="settings-row checkbox-row">
|
||||
<label>Queue beim Start wiederherstellen</label>
|
||||
<input type="checkbox" class="settings-autosave" id="resumeQueueOnLaunchInput" ${globalSettings.resumeQueueOnLaunch === false ? '' : 'checked'}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section-label">Log</div>
|
||||
<div class="settings-row">
|
||||
<label>FileUploader Log</label>
|
||||
<input type="text" class="key-input settings-autosave" id="logFilePathInput" value="${escapeAttr(globalSettings.logFilePath || '')}" placeholder="Standardpfad verwenden">
|
||||
<button class="btn btn-xs btn-secondary" id="chooseLogFilePathBtn">Ordner wählen</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(generalPanel);
|
||||
@ -1438,7 +1480,8 @@ async function saveSettings(options = {}) {
|
||||
resumeQueueOnLaunch: !!document.getElementById('resumeQueueOnLaunchInput')?.checked,
|
||||
parallelUploadCount: Math.max(0, Math.min(100, parseInt(document.getElementById('parallelUploadCountInput')?.value || '0', 10) || 0)),
|
||||
scaleParallelUploads: !!document.getElementById('scaleParallelUploadsInput')?.checked,
|
||||
removeFromQueueOnDone: !!document.getElementById('removeFromQueueOnDoneInput')?.checked
|
||||
removeFromQueueOnDone: !!document.getElementById('removeFromQueueOnDoneInput')?.checked,
|
||||
globalMaxSpeedKbs: Math.max(0, Math.round((parseFloat(document.getElementById('globalMaxSpeedMbsInput')?.value || '0') || 0) * 1024))
|
||||
};
|
||||
|
||||
for (const name of HOSTERS) {
|
||||
@ -1785,12 +1828,13 @@ async function loadHistory() {
|
||||
const dt = formatDateTime(batch.timestamp || new Date());
|
||||
for (const file of (batch.files || [])) {
|
||||
for (const result of (file.results || [])) {
|
||||
const isErrorLike = result.status === 'error' || result.status === 'aborted';
|
||||
if (result.status === 'aborted') continue;
|
||||
const isError = result.status === 'error';
|
||||
historyRowsData.push({
|
||||
date: dt.text, dateTs: dt.ts,
|
||||
filename: file.name || '', host: result.hoster || '',
|
||||
link: isErrorLike ? `[${result.status === 'aborted' ? 'Abgebrochen' : 'Fehler'}] ${result.error || ''}` : (result.download_url || result.embed_url || ''),
|
||||
isError: isErrorLike, order: order++
|
||||
link: isError ? `[Fehler] ${result.error || ''}` : (result.download_url || result.embed_url || ''),
|
||||
isError, order: order++
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -2140,5 +2184,41 @@ function showCopyToast(msg) {
|
||||
toast._timer = setTimeout(() => toast.classList.remove('show'), 1500);
|
||||
}
|
||||
|
||||
// --- Resize handle for recent-files panel ---
|
||||
{
|
||||
const resizer = document.getElementById('recentFilesResizer');
|
||||
const panel = document.getElementById('recentFilesPanel');
|
||||
if (resizer && panel) {
|
||||
let startY = 0;
|
||||
let startH = 0;
|
||||
|
||||
resizer.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
startY = e.clientY;
|
||||
startH = panel.getBoundingClientRect().height;
|
||||
resizer.classList.add('dragging');
|
||||
document.body.style.cursor = 'ns-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
|
||||
const onMove = (e2) => {
|
||||
const delta = startY - e2.clientY;
|
||||
const newH = Math.max(60, Math.min(window.innerHeight * 0.7, startH + delta));
|
||||
panel.style.flex = `0 0 ${newH}px`;
|
||||
};
|
||||
|
||||
const onUp = () => {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
resizer.classList.remove('dragging');
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- Start ---
|
||||
init();
|
||||
|
||||
@ -93,6 +93,7 @@
|
||||
<button class="btn btn-xs btn-secondary" id="retryFailedBtn" style="display:none">Fehlgeschlagene erneut</button>
|
||||
</div>
|
||||
|
||||
<div class="resize-handle" id="recentFilesResizer"></div>
|
||||
<div class="recent-files-panel" id="recentFilesPanel">
|
||||
<div class="recent-files-header">
|
||||
<h3>Files</h3>
|
||||
|
||||
@ -346,6 +346,17 @@ body {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
flex: 0 0 5px;
|
||||
cursor: ns-resize;
|
||||
background: var(--border);
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.resize-handle:hover, .resize-handle.dragging {
|
||||
background: var(--accent);
|
||||
}
|
||||
.recent-files-panel {
|
||||
flex: 0 0 180px;
|
||||
display: flex;
|
||||
@ -616,6 +627,17 @@ body {
|
||||
.key-input:focus, .hs-input:focus { border-color: var(--accent); outline: none; }
|
||||
.hs-input { max-width: 100px; }
|
||||
.hint { font-size: 10px; color: var(--text-dim); }
|
||||
.settings-section-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-dim);
|
||||
margin: 10px 0 6px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.settings-section-label:first-child { margin-top: 0; }
|
||||
|
||||
.toggle-vis {
|
||||
background: transparent;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user