Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7749699830 | ||
|
|
de371e56a3 | ||
|
|
b93617ace9 | ||
|
|
82e0163d3f | ||
|
|
05fae3209d | ||
|
|
0f72478a2e | ||
|
|
f0fb5f881f | ||
|
|
d3fda31243 |
29
main.js
29
main.js
@ -1379,6 +1379,17 @@ ipcMain.handle('select-folder', async () => {
|
|||||||
});
|
});
|
||||||
if (result.canceled || !result.filePaths.length) return null;
|
if (result.canceled || !result.filePaths.length) return null;
|
||||||
|
|
||||||
|
const files = [];
|
||||||
|
for (const folder of result.filePaths) await walkFolderAsync(folder, files);
|
||||||
|
return files.length > 0 ? files.map(f => f.path) : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('select-folder-with-sizes', async () => {
|
||||||
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
|
properties: ['openDirectory', 'multiSelections']
|
||||||
|
});
|
||||||
|
if (result.canceled || !result.filePaths.length) return null;
|
||||||
|
|
||||||
const files = [];
|
const files = [];
|
||||||
for (const folder of result.filePaths) await walkFolderAsync(folder, files);
|
for (const folder of result.filePaths) await walkFolderAsync(folder, files);
|
||||||
return files.length > 0 ? files : null;
|
return files.length > 0 ? files : null;
|
||||||
@ -1396,7 +1407,11 @@ async function walkFolderAsync(rootDir, outFiles) {
|
|||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const full = path.join(dir, entry.name);
|
const full = path.join(dir, entry.name);
|
||||||
if (entry.isDirectory()) stack.push(full);
|
if (entry.isDirectory()) stack.push(full);
|
||||||
else if (entry.isFile()) outFiles.push(full);
|
else if (entry.isFile()) {
|
||||||
|
let size = 0;
|
||||||
|
try { size = (await fsp.stat(full)).size; } catch {}
|
||||||
|
outFiles.push({ path: full, name: entry.name, size });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ((++scanned % 8) === 0) await new Promise(setImmediate);
|
if ((++scanned % 8) === 0) await new Promise(setImmediate);
|
||||||
}
|
}
|
||||||
@ -1408,6 +1423,18 @@ ipcMain.handle('resolve-folder-files', async (_event, folderPath) => {
|
|||||||
return files;
|
return files;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('get-file-sizes', async (_event, paths) => {
|
||||||
|
if (!Array.isArray(paths)) return {};
|
||||||
|
const fsp = fs.promises;
|
||||||
|
const out = {};
|
||||||
|
let i = 0;
|
||||||
|
for (const p of paths) {
|
||||||
|
try { out[p] = (await fsp.stat(p)).size; } catch { out[p] = 0; }
|
||||||
|
if ((++i % 32) === 0) await new Promise(setImmediate);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('start-upload', (_event, payload) => {
|
ipcMain.handle('start-upload', (_event, payload) => {
|
||||||
const config = configStore.load();
|
const config = configStore.load();
|
||||||
const files = payload && Array.isArray(payload.files) ? payload.files : [];
|
const files = payload && Array.isArray(payload.files) ? payload.files : [];
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "3.3.54",
|
"version": "3.3.58",
|
||||||
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -30,7 +30,9 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
// File selection
|
// File selection
|
||||||
selectFiles: () => ipcRenderer.invoke('select-files'),
|
selectFiles: () => ipcRenderer.invoke('select-files'),
|
||||||
selectFolder: () => ipcRenderer.invoke('select-folder'),
|
selectFolder: () => ipcRenderer.invoke('select-folder'),
|
||||||
|
selectFolderWithSizes: () => ipcRenderer.invoke('select-folder-with-sizes'),
|
||||||
resolveFolderFiles: (folderPath) => ipcRenderer.invoke('resolve-folder-files', folderPath),
|
resolveFolderFiles: (folderPath) => ipcRenderer.invoke('resolve-folder-files', folderPath),
|
||||||
|
getFileSizes: (paths) => ipcRenderer.invoke('get-file-sizes', paths),
|
||||||
|
|
||||||
// Upload control
|
// Upload control
|
||||||
startUpload: (payload) => ipcRenderer.invoke('start-upload', payload),
|
startUpload: (payload) => ipcRenderer.invoke('start-upload', payload),
|
||||||
|
|||||||
114
renderer/app.js
114
renderer/app.js
@ -99,6 +99,7 @@ async function init() {
|
|||||||
syncSelectedUploadHosters();
|
syncSelectedUploadHosters();
|
||||||
restoreQueueStateFromConfig();
|
restoreQueueStateFromConfig();
|
||||||
await _autoDeduplicateFromLog();
|
await _autoDeduplicateFromLog();
|
||||||
|
_hydrateMissingJobSizes();
|
||||||
renderHosterSummary();
|
renderHosterSummary();
|
||||||
renderHosterModal();
|
renderHosterModal();
|
||||||
renderSettings();
|
renderSettings();
|
||||||
@ -695,11 +696,12 @@ async function addDroppedFiles(fileList) {
|
|||||||
const folderFiles = await window.api.resolveFolderFiles(filePath);
|
const folderFiles = await window.api.resolveFolderFiles(filePath);
|
||||||
if (folderFiles && folderFiles.length > 0) {
|
if (folderFiles && folderFiles.length > 0) {
|
||||||
for (const fp of folderFiles) {
|
for (const fp of folderFiles) {
|
||||||
if (!existingPaths.has(fp)) {
|
const p = typeof fp === 'string' ? fp : (fp && fp.path);
|
||||||
const name = fp.split('\\').pop().split('/').pop();
|
if (!p || existingPaths.has(p)) continue;
|
||||||
newFiles.push({ path: fp, name, size: null });
|
const name = typeof fp === 'string' ? p.split('\\').pop().split('/').pop() : (fp.name || p.split('\\').pop().split('/').pop());
|
||||||
existingPaths.add(fp);
|
const size = typeof fp === 'string' ? null : (fp.size || 0);
|
||||||
}
|
newFiles.push({ path: p, name, size });
|
||||||
|
existingPaths.add(p);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -730,28 +732,52 @@ async function pickFiles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function pickFolder() {
|
async function pickFolder() {
|
||||||
|
const richFiles = window.api.selectFolderWithSizes ? await window.api.selectFolderWithSizes() : null;
|
||||||
|
if (richFiles && Array.isArray(richFiles)) { addPathsToQueue(richFiles); return; }
|
||||||
const paths = await window.api.selectFolder();
|
const paths = await window.api.selectFolder();
|
||||||
if (!paths) return;
|
if (!paths) return;
|
||||||
addPathsToQueue(paths);
|
addPathsToQueue(paths);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPathsToQueue(paths) {
|
function addPathsToQueue(paths) {
|
||||||
// Build path-Set once so dedup is O(1) per candidate instead of O(n+m).
|
|
||||||
// Matters when the user picks a folder with thousands of files.
|
|
||||||
const existing = new Set();
|
const existing = new Set();
|
||||||
for (const f of selectedFiles) existing.add(f.path);
|
for (const f of selectedFiles) existing.add(f.path);
|
||||||
for (const f of _pendingFiles) existing.add(f.path);
|
for (const f of _pendingFiles) existing.add(f.path);
|
||||||
|
|
||||||
const newFiles = [];
|
const newFiles = [];
|
||||||
for (const p of paths) {
|
const pendingSizeFetch = [];
|
||||||
if (existing.has(p)) continue;
|
for (const entry of paths) {
|
||||||
|
const p = typeof entry === 'string' ? entry : (entry && entry.path);
|
||||||
|
if (!p || existing.has(p)) continue;
|
||||||
existing.add(p);
|
existing.add(p);
|
||||||
const name = p.split('\\').pop().split('/').pop();
|
const name = typeof entry === 'string' ? p.split('\\').pop().split('/').pop() : (entry.name || p.split('\\').pop().split('/').pop());
|
||||||
newFiles.push({ path: p, name, size: null });
|
const size = typeof entry === 'string' ? null : (entry.size || 0);
|
||||||
|
newFiles.push({ path: p, name, size });
|
||||||
|
if (size === null || size === undefined || size === 0) pendingSizeFetch.push(p);
|
||||||
}
|
}
|
||||||
if (newFiles.length > 0) {
|
if (newFiles.length > 0) {
|
||||||
_pendingFiles.push(...newFiles);
|
_pendingFiles.push(...newFiles);
|
||||||
openHosterModal();
|
openHosterModal();
|
||||||
|
if (pendingSizeFetch.length > 0 && window.api.getFileSizes) {
|
||||||
|
window.api.getFileSizes(pendingSizeFetch).then((sizeMap) => {
|
||||||
|
if (!sizeMap || typeof sizeMap !== 'object') return;
|
||||||
|
let changed = false;
|
||||||
|
for (const f of _pendingFiles) {
|
||||||
|
if (sizeMap[f.path] && (!f.size || f.size === 0)) { f.size = sizeMap[f.path]; changed = true; }
|
||||||
|
}
|
||||||
|
for (const f of selectedFiles) {
|
||||||
|
if (sizeMap[f.path] && (!f.size || f.size === 0)) { f.size = sizeMap[f.path]; changed = true; }
|
||||||
|
}
|
||||||
|
for (const j of queueJobs) {
|
||||||
|
if (sizeMap[j.file] && (!j.bytesTotal || j.bytesTotal === 0)) { j.bytesTotal = sizeMap[j.file]; changed = true; }
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
_queueStatsCache = null;
|
||||||
|
if (typeof renderQueueTable === 'function') renderQueueTable();
|
||||||
|
if (typeof updateStatusBar === 'function') updateStatusBar();
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -972,12 +998,54 @@ function scheduleStatusChangeUpdate() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _hydrateMissingJobSizes(jobsLike) {
|
||||||
|
if (!window.api || !window.api.getFileSizes) return;
|
||||||
|
const paths = [];
|
||||||
|
const seen = new Set();
|
||||||
|
const source = Array.isArray(jobsLike) ? jobsLike : queueJobs;
|
||||||
|
for (const j of source) {
|
||||||
|
if (!j || !j.file) continue;
|
||||||
|
if (j.bytesTotal && j.bytesTotal > 0) continue;
|
||||||
|
if (seen.has(j.file)) continue;
|
||||||
|
seen.add(j.file);
|
||||||
|
paths.push(j.file);
|
||||||
|
}
|
||||||
|
if (paths.length === 0) return;
|
||||||
|
window.api.getFileSizes(paths).then((sizeMap) => {
|
||||||
|
if (!sizeMap || typeof sizeMap !== 'object') return;
|
||||||
|
let changed = false;
|
||||||
|
for (const j of queueJobs) {
|
||||||
|
if (sizeMap[j.file] && (!j.bytesTotal || j.bytesTotal === 0)) {
|
||||||
|
j.bytesTotal = sizeMap[j.file];
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const f of selectedFiles) {
|
||||||
|
if (sizeMap[f.path] && (!f.size || f.size === 0)) f.size = sizeMap[f.path];
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
_queueStatsCache = null;
|
||||||
|
if (typeof renderQueueTable === 'function') renderQueueTable();
|
||||||
|
if (typeof updateStatusBar === 'function') updateStatusBar();
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _formatUploadedSize(job) {
|
||||||
|
const bt = job.bytesTotal || 0;
|
||||||
|
const bu = job.bytesUploaded || 0;
|
||||||
|
const s = job.status;
|
||||||
|
if (s === 'preview') return bt > 0 ? formatSize(bt) : '...';
|
||||||
|
if (s === 'queued' || s === 'getting-server' || s === 'retrying') {
|
||||||
|
return bt > 0 ? `${formatSize(bu)} / ${formatSize(bt)}` : '...';
|
||||||
|
}
|
||||||
|
return `${formatSize(bu)} / ${formatSize(bt)}`;
|
||||||
|
}
|
||||||
|
|
||||||
function buildRowHtml(job) {
|
function buildRowHtml(job) {
|
||||||
const statusClass = `status-${job.status}`;
|
const statusClass = `status-${job.status}`;
|
||||||
const rowClass = `queue-row ${statusClass}${selectedJobIds.has(job.id) ? ' selected' : ''}`;
|
const rowClass = `queue-row ${statusClass}${selectedJobIds.has(job.id) ? ' selected' : ''}`;
|
||||||
const uploadedSize = job.status === 'preview'
|
const uploadedSize = _formatUploadedSize(job);
|
||||||
? (job.bytesTotal > 0 ? formatSize(job.bytesTotal) : '...')
|
|
||||||
: `${formatSize(job.bytesUploaded)} / ${formatSize(job.bytesTotal)}`;
|
|
||||||
const statusText = getStatusText(job);
|
const statusText = getStatusText(job);
|
||||||
const elapsed = formatTime(job.elapsed);
|
const elapsed = formatTime(job.elapsed);
|
||||||
const remaining = formatTime(job.remaining);
|
const remaining = formatTime(job.remaining);
|
||||||
@ -1007,9 +1075,7 @@ function buildRowHtml(job) {
|
|||||||
// In-place update of a single row's cells (avoids full innerHTML rebuild)
|
// In-place update of a single row's cells (avoids full innerHTML rebuild)
|
||||||
function _updateRowInPlace(tr, job) {
|
function _updateRowInPlace(tr, job) {
|
||||||
const statusClass = `status-${job.status}`;
|
const statusClass = `status-${job.status}`;
|
||||||
const uploadedSize = job.status === 'preview'
|
const uploadedSize = _formatUploadedSize(job);
|
||||||
? (job.bytesTotal > 0 ? formatSize(job.bytesTotal) : '...')
|
|
||||||
: `${formatSize(job.bytesUploaded)} / ${formatSize(job.bytesTotal)}`;
|
|
||||||
const statusText = getStatusText(job);
|
const statusText = getStatusText(job);
|
||||||
const elapsed = formatTime(job.elapsed);
|
const elapsed = formatTime(job.elapsed);
|
||||||
const remaining = formatTime(job.remaining);
|
const remaining = formatTime(job.remaining);
|
||||||
@ -1703,6 +1769,7 @@ async function startUpload() {
|
|||||||
if (uploading) return;
|
if (uploading) return;
|
||||||
uploading = true; // set immediately to prevent double-click race
|
uploading = true; // set immediately to prevent double-click race
|
||||||
updateQueueActionButtons();
|
updateQueueActionButtons();
|
||||||
|
_hydrateMissingJobSizes();
|
||||||
|
|
||||||
const hosters = getSelectedHosters();
|
const hosters = getSelectedHosters();
|
||||||
if (queueJobs.length === 0 && selectedFiles.length > 0) {
|
if (queueJobs.length === 0 && selectedFiles.length > 0) {
|
||||||
@ -1775,10 +1842,13 @@ function _markSkippedJobs(result) {
|
|||||||
|
|
||||||
async function startSelectedUpload() {
|
async function startSelectedUpload() {
|
||||||
if (uploading) {
|
if (uploading) {
|
||||||
// Batch already running — add selected jobs (queued/error/aborted/skipped) to running batch
|
_hydrateMissingJobSizes();
|
||||||
// Upload-manager has duplicate protection (skips jobs already tracked)
|
const addable = queueJobs.filter(j => selectedJobIds.has(j.id) && isStartableQueueStatus(j.status));
|
||||||
const addable = queueJobs.filter(j => selectedJobIds.has(j.id) && ['queued', 'error', 'aborted', 'skipped'].includes(j.status));
|
if (addable.length === 0) {
|
||||||
if (addable.length > 0) {
|
if (selectedJobIds.size > 0) showCopyToast('Keine startbaren Jobs ausgewählt (alle laufen schon oder sind fertig).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
{
|
||||||
addable.forEach(j => {
|
addable.forEach(j => {
|
||||||
j.status = 'queued'; j.error = null; j.result = null;
|
j.status = 'queued'; j.error = null; j.result = null;
|
||||||
j.bytesUploaded = 0; j.speedKbs = 0; j.progress = 0; j.uploadId = null;
|
j.bytesUploaded = 0; j.speedKbs = 0; j.progress = 0; j.uploadId = null;
|
||||||
@ -1823,7 +1893,6 @@ async function startSelectedUpload() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
uploading = true; // set immediately to prevent double-click race
|
uploading = true; // set immediately to prevent double-click race
|
||||||
updateQueueActionButtons();
|
updateQueueActionButtons();
|
||||||
@ -2083,7 +2152,6 @@ function handleBatchDone(summary) {
|
|||||||
|
|
||||||
lastUploadStats = { state: 'idle', globalSpeedKbs: 0, totalBytes: lastUploadStats.totalBytes, elapsed: lastUploadStats.elapsed, activeJobs: 0 };
|
lastUploadStats = { state: 'idle', globalSpeedKbs: 0, totalBytes: lastUploadStats.totalBytes, elapsed: lastUploadStats.elapsed, activeJobs: 0 };
|
||||||
updateStatusBar();
|
updateStatusBar();
|
||||||
_maybeShowBatchSummary(summary);
|
|
||||||
_refreshSessionFailedSnapshot();
|
_refreshSessionFailedSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -342,22 +342,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal" id="batchSummaryModal" style="display:none">
|
|
||||||
<div class="modal-content" style="max-width:680px">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2>Batch-Zusammenfassung</h2>
|
|
||||||
<button class="icon-btn" id="batchSummaryClose" aria-label="Schließen">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div id="batchSummaryList"></div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="btn btn-secondary" id="batchSummaryRetryTransient">Transiente erneut hochladen</button>
|
|
||||||
<button class="btn btn-primary" id="batchSummaryRetryAll">Alle Fehler erneut versuchen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="../lib/queue-prune.js"></script>
|
<script src="../lib/queue-prune.js"></script>
|
||||||
<script src="../lib/queue-dedup.js"></script>
|
<script src="../lib/queue-dedup.js"></script>
|
||||||
<script src="../lib/log-mode.js"></script>
|
<script src="../lib/log-mode.js"></script>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user