Previously: clicking "Erneut versuchen" after a batch had already
finished spawned a fresh UploadManager with empty _failedAccounts and
_accountOverrides. The first retry then burned the full retry budget
on the account we already knew was dead (e.g. disk-space-full byse
account) before rotation kicked in again — same problem we fixed for
within-batch flow but for across-batch flow.
- main.js: two module-level maps (_sessionFailedAccounts,
_sessionAccountOverrides) cache rotation state across batches in the
same app session. Populated on account-failed and on both
switchAccount paths (event-driven + save-config re-resolve).
- lib/upload-manager.js: startBatch(tasks, opts) accepts
primeFailedAccounts + primeOverrides. State is still cleared first
(legacy behaviour for callers without opts), then re-primed from the
passed session state. batch-start rot-log entry reports how many
entries were primed for diagnostics.
- Tests: prime priority is honored (pre-job-swap fires on first
attempt, no fast-fail, no upload to acc1); back-compat for callers
that don't pass opts.
App restart remains the reset signal — matches the "neuer Tag, acc1
hat vielleicht wieder Platz" expectation.
Three small, unrelated reliability improvements bundled:
1. lib/hosters.js (_resolveByseUploadByName): drop the "only one new
file → claim it" fallback. Under parallel byse uploads, job A's
poller could claim job B's newly-uploaded file and return the wrong
URL. Now requires exact normalized name match. Trade-off: a few
false negatives if byse rewrites the filename beyond our
normalizer, but parallel correctness wins.
2. tests/upload-manager.test.js: pin the transient-network classifier
behaviour with 2 new tests covering common transient strings
(ENOTFOUND, ECONNRESET, socket hang up, fetch failed, EAI_AGAIN…)
and verifying real account-level / file-rejected errors are NOT
misclassified as transient. Baseline stays clean: 82/82 green.
3. main.js: log process.memoryUsage() snapshot at batch-start and
batch-done. One line each — harmless in the happy path, gives us
the data points needed to spot long-session RSS/heap growth across
batches without DevTools instrumentation.
Non-uploading progress events (queued/getting-server/retrying/done/
error/aborted/skipped) were firing renderQueueTable +
updateQueueActionButtons + updateStatusBar + updateStatsPanel
synchronously on EVERY event. At batch start, 500 jobs going
preview→queued→getting-server within milliseconds meant ~2000 sync DOM
updates — visible jank on large batches.
New scheduleStatusChangeUpdate() uses requestAnimationFrame to coalesce
the four-helper call into at most one run per frame (~60 Hz). Functional
result is identical; the user just sees smooth flips instead of a
briefly frozen renderer.
The uploading-progress throttle (200ms) is unchanged since those events
are much more frequent and the user doesn't need 60 Hz upload-byte
updates.
At 500+ queued jobs these lines bloated upload-debug.log with
megabyte-sized entries per batch-start and added visible latency to
the IPC handler. Log sizes only.
Three related gaps closed so one full byse account stops wasting
attempts on every subsequent job and later-added accounts get picked
up without an app restart.
1. Pre-job-swap moved BEHIND the semaphore acquire. At scale (500 jobs
/ 1 slot) every worker was checking _failedAccounts at spawn time
before the first upload had even tried — so none of them saw the
failed state. Now each worker re-checks right before its first
upload attempt.
2. save-config IPC handler re-resolves fallbacks for any account that
is already in _failedAccounts but has no override set. Previously
account-failed only fired once per account, so a config change
after the first mark-failed was silently ignored and the batch
stayed stuck on the dead account until the app restarted.
3. UploadManager exposes getFailedAccountKeys() and getOverride(hoster)
so main.js can drive the late re-resolve without poking private
fields.
4 new tests: pre-job-swap after semaphore, getters contract, fresh
manager resets learned state, late-added fallback is honored by
subsequent jobs. 80/80 green.
Byse rejects uploads with status like "not enough disk space on your
account" when the account's storage is exhausted. The parser was
flagging every non-OK status as err.fileRejected=true, and the upload-
manager classifier additionally matched the generic "lehnte Datei ab"
prefix as file-rejected. Result: rotation was skipped on a full account
and every subsequent file failed on the same dead account.
- hosters.js: byse parser now distinguishes account-level phrases
(disk space / storage / quota / insufficient / account full) and sets
err.accountError=true for those. File-specific failures (Duplicate,
wrong format, size) keep err.fileRejected=true.
- upload-manager.js: _isFileRejectedError no longer matches the generic
"lehnte Datei ab" prefix and short-circuits when err.accountError is
true. _shouldSkipRetryOnAccountError honors the flag and has added
regex patterns as a safety net.
- Tests: 5 new unit tests covering disk-space/account-level/duplicate
and the accountError-wins-over-fileRejected precedence.
retrySelectedJobs() was calling renderQueueTable + updateQueueActionButtons
+ updateStatusBar and then immediately awaiting startSelectedUpload(),
which runs the exact same trio right after. At 500+ failed jobs the
double render/sort/button-refresh freezes the UI for several seconds
after clicking "Erneut versuchen".
Drop the outer render trio — startSelectedUpload's one is enough. The
inner call sees the freshly-mutated job state in the same tick, so the
visible result is identical with half the work.
User reported uploads appearing on the byse dashboard (2+ GB MKV,
Server #262, status OK) even though the app marked them failed. The
byse API sometimes replies with msg=OK + files:[{filecode:"",
status:"Not video file format"}] — a misleading response where the
file is actually being accepted and gets its filecode assigned
asynchronously.
- Before the upload POST, snapshot the current /api/file/list to
know what was already there.
- If parseByseResult returns an empty filecode (or throws a
fileRejected error), poll /api/file/list up to 15 × 2s looking
for a new file_code matching the uploaded filename (case/punct
normalized, extension stripped).
- If matched, return the real download/embed URLs and let the
upload complete as successful. Only throw the parser's error
if polling also finds nothing.
The log revealed byse's true response shape for rejected files:
{ msg: 'OK', status: 200, files: [{ filecode: '', status: 'Not video file format' }] }
HTTP 200 + msg=OK made the old code treat it as 'success but no
file_code'. The real error ('Not video file format') was buried in
files[0].status. parseByseResult now surfaces that with a dedicated
err.fileRejected flag so the rotation layer can distinguish
file-specific vs account-specific failures.
Rotation behavior:
- file-rejected errors: no retries, no account blacklist, no
rotation. The same file is going to get the same verdict on
any account, so skip straight to 'error' status and keep the
account available for other files in the batch.
- network errors (already handled): no account blacklist either.
- everything else: unchanged (retry then rotate).
Also added pattern matches for common rejections (Duplicate, File
too small/large, Unsupported format, etc.) so other hosters'
per-file errors get the same treatment.
Two bugs visible in the user's rotation log:
1. 'error=OK' for byse.sx — the server returned a payload with
msg='OK' and no file_code anywhere we recognized. Our generic
uploadFile threw the bare 'OK' as the error message, which is
useless and misleading. Now when we see an ok-ish msg without
the expected file_code we throw a descriptive error that
includes the first ~400 bytes of the payload so the next time
it happens we can see what's actually being returned (API
changed, new field name, etc.).
2. 'getaddrinfo ENOTFOUND s1055.filemoon' was marking accounts as
permanently failed, blacklisting BOTH byse accounts within the
same batch even though neither was the actual problem — filemoon
(byse's storage backend) briefly had a DNS blip. Added
_isTransientNetworkError() covering DNS/ECONNRESET/ETIMEDOUT/etc.
When all retries on an account exhaust with a transient error,
we now fail just that file and emit 'skip-rotation-transient'
instead of adding the account to _failedAccounts. Other files
in the same batch still get a fresh try on the same account.
Four user-visible lag sources tracked down from a wider audit:
- Tab click was running three full querySelectorAll walks per click
(remove active from all tabs, all views, find new tab). Replaced
with delegated listener on the tab bar plus cached node maps;
tab switching is now O(1) and a no-op when clicking the active tab.
- saveSettings awaited saveHosterSettings + saveGlobalSettings
serially and then re-fetched the full config from main. With
autosave firing on every keystroke this added 100–200ms of IPC
stall per input change. The two saves now run in parallel and the
post-save getConfig refetch is gone — we know the new state.
- showContextMenu rebuilt hosterCounts (queueJobs.forEach) on every
right-click. Replaced with a length-keyed cache; right-click on a
5000-job queue no longer pauses while counting.
- Recent-panel shift-click was querying every .recent-file-row in
the DOM and re-parsing data-order. Reuses _recentSortCache.result
instead, O(visible) vs O(N).
When the configured log path isn't writable and we fall back to
Desktop/userData, the working fallback now gets saved into
globalSettings.logFilePath automatically. Benefits:
- Next session writes directly to the known-working path, no
fallback ladder, no recurring toast warning.
- The Settings input reflects the actual path in use, so users
don't stay confused about where their uploads are being logged.
- Live update via IPC — if the Settings view is currently open,
the input value updates without needing a view switch.
Daily-log mode is handled: we strip the -YYYY-MM-DD suffix before
persisting so tomorrow's auto-rotation doesn't double-date the
filename.
alert() in Electron halts the renderer main thread until the user
clicks OK — the upload table, status bar and progress all freeze.
During a 170-file batch the dialog popped up mid-upload and froze
everything for however long the user took to dismiss it (which is
why stats updates lagged to one every 3-5s instead of the usual 1s
cadence).
Replaced with the same showCopyToast used elsewhere, with an 8s
duration so the message is still readable. showCopyToast now accepts
an optional durationMs argument.
When multiple jobs run in parallel on the same hoster and the primary
account starts failing, the first job marks it failed + triggers
rotation. The second job's retries then also exhaust on the same
(already-failed) primary — but the old while-condition
`!_failedAccounts.has(...)` short-circuited the whole rotation loop
for anything already marked, so the second job went straight to
final-error even though a resolved override was sitting right there.
Now the loop always checks for an available override; it only skips
the mark-failed + emit step if the account was already marked by a
concurrent job. Fixed visible symptom: first job rotates A→B, every
other job in the same batch that hit A got final-error instead of
also switching to B.
Also extended fast-fail patterns to include 429 (Too many requests),
CSRF-Token / 'Bist du eingeloggt' — both were showing up as the
primary failure mode in real uploads and were wasting 5 retries
each.
Three related improvements that landed together while wiring up the
rotation log infrastructure:
- Fast-fail classifier: errors that clearly indicate the account
itself is the problem (rate limit, quota, banned/suspended, auth
failure, 401/403, 'Kein Upload-Server' from delivery-node etc.)
now skip the remaining retries and go straight to rotation. No
more waiting 5 × 3s between retries just to end up rotating
anyway. Emits a 'fast-fail' rot-log event so the shortcut is
visible.
- Settings: 'Öffnen' button next to the log-file-path input reveals
the active log file (or its directory if nothing's written yet)
in the OS file manager, so users don't have to remember paths.
- rotLog() writes the rotation log synchronously. Only a handful
of events fire per batch; the 500ms flush batching was saving
nothing and made the file look empty when users checked right
after an event. (The main debug log still uses the batched async
path — that one is high-volume.)
To trace whether the fallback chain actually engages during real uploads,
every rotation decision now emits a structured 'rot-log' event from the
upload-manager. main.js persists each event to a new account-rotation.log
(same directory as fileuploader.log; falls back to Desktop then userData)
and also mirrors it into the main debug log with a [ROT] prefix for
single-file grepping.
Logged events:
- batch-start (clears _failedAccounts / _accountOverrides)
- pre-job-swap / pre-job-swap-blocked (job picks override before first try)
- retries-exhausted / mark-failed (enters rotation loop)
- rotate (switched to new account, retry starting)
- rotation-end (no override / override already failed)
- final-error (all accounts exhausted)
- switchAccount (main resolved the next fallback)
The renderer shows a toast on 'rotate', 'rotation-end' and 'final-error'
so fallback behavior is visible live instead of buried in logs.
The Accounts view rebuilt the whole list on every enable/disable/
check/reorder. Each render destroyed and recreated four click
listeners plus five drag listeners per card (20 accounts = 180
listeners cycled per click), then ran an IPC getConfig round-trip
on top. Typing-fast enable/disable toggles felt sludgy.
- Single delegated click handler on the accounts container.
- Single delegated set of drag/drop handlers (one per event type,
not per card).
- Listeners are bound once on first render, never rebound.
- updateAccountCard(accountId) swaps just the one affected card's
DOM node when its state changes. toggleAccount / checkSingleAccount
use that instead of calling renderAccounts.
- Drag-and-drop reorder moves the DOM node in place and re-renders
only the priority badges of the affected group — no container
rebuild, no getConfig refetch.
Captured the real browser upload POST and compared to our request.
Two corrections:
- The file field is named 'file', not 'file_0'. The XFS-indexed
naming was a bad guess — the current transit accepts only 'file'.
- The form also needs 'to_json=1' (forces JSON response instead of
an HTML redirect page, matching what the browser submits) and
'fld_id=0' (destination folder, 0 = root). Dropped upload_type,
srv_tmp_url, utype — those were XFS remnants and aren't part of
the current server's contract.
- Response shape is now { status: 'OK', file_code, msg } instead of
the older { files: [...] } / { result: ... } XFS variants; the
parser handles all three plus carries the server's msg forward
on explicit rejections.
The transit server runs nginx-upload-progress and requires an
X-Progress-ID query parameter on the POST URL to finalize the
upload session. Without it the server accepts all bytes but never
sends the response — matches the reported 99%-stuck behavior. The
browser appends it automatically before submit; we now do the same.
Upload stalled at 99% because we were sending vidmoly.me cookies to
*.vmwesa.online (transit server rejects them silently). Browsers never
send those cross-origin. Now we omit the Cookie header and match the
Origin/Referer the browser uses. Also added the full classic XFS field
set (upload_type, sess_id, srv_tmp_url, utype) in the order the
server's handler expects.
The SPA redesign killed the old XFS form POST at / with op=login.
The new flow is a JSON POST to /api/auth/login that returns a
vidmoly_session HttpOnly cookie, which is what /api/upload/config
actually authenticates against.
After login we also probe /api/upload/config once to fail fast if
the session was issued but not actually valid for uploads.
The old /my HTML check failed because it couldn't distinguish an XFS
session from a full SPA session. Since /api/upload/config is what the
upload actually needs, probe it directly after login — 200 JSON with
sess_id/upload_url means we're good, anything else means we're out.
The Vidmoly SPA redesign removed the /?op=upload HTML form — the old
regex-scrape of hidden inputs no longer works. The site now exposes
GET /api/upload/config which returns { sess_id, upload_url } plus the
allowed extensions. Rewrote getUploadParams() to use that endpoint;
the rest of the multipart upload flow (sess_id + utype + file_0) is
the same classic XFS shape.