Root cause of "4 accounts configured, rotation only ever uses 1-3": byse
returns HTTP 200 + per-file status "Not video file format" for valid MKVs
above an account's size tier (proven from the live rotation log: account
4ha0 accepted 1158 files up to 2.62 GB and deterministically rejected all
6 files >= 2.81 GB with that status, each costing a full ~20 min upload).
The parser tagged this fileRejected, which (a) skipped the 30s recovery
poll that was originally BUILT for this misleading status (guard regression
from "byse skips 30s recovery poll on explicit reject") and (b) made the
upload manager skip rotation entirely - so the remaining accounts were
never tried and the files failed forever. Account selection is
primary-first failover, so a later account only ever runs when every
earlier one is hard-failed or disabled, which matches the reported
"account 4 only gets used when I disable the others".
The fix, layered:
- parseByseResult flags "Not video file format" as err.suspectReject
(alongside fileRejected). Genuine rejections (Duplicate, too small,
account-level disk-full) behave exactly as before.
- uploadFile runs the byse recovery poll again for suspect rejections
(the file often registers asynchronously despite the status), but skips
it when the caller's file probe says the upload is genuinely not a video
(new opts.probeIsVideoLike, threaded from the upload manager's probe).
Poll helpers now swallow an abort during their sleep and return null so
a cancelled poll surfaces the original error instead of a bare
AbortError.
- upload-manager: on a suspect rejection of a probe-verified video, the
file gets ONE attempt on each remaining pool account (new accountPools
from main.js, refreshed on save-config) - WITHOUT blacklisting the
rejecting account, which keeps working for smaller files. A per-batch
size memo (hoster:account -> smallest rejected size) plus a known-good
account preference prevent re-uploading every following oversized file
through the whole chain: the second file skips known-limited accounts
outright and goes straight to the account that took the first one.
Alternates that fail with a genuine account error (quota/ban/full) are
marked failed in-batch so no later file burns a multi-GB upload on them;
deliberately no account-failed emit there, as repointing the hoster-wide
override would reroute normal-sized files away from a healthy primary.
- Rotation hardening from the adversarial review: the rotated-to retry
loop now fast-breaks on file-rejected/hoster-transient errors instead of
burning maxAttempts full uploads, and a file-class error on a rotated
account no longer blacklists that account (it fails only the file).
Cancelling mid-alternates or mid-rotation now records the job as
aborted instead of error (this also stops the batch webhook from firing
on fully user-cancelled batches). New upload-failure rot-logs are
guarded against logging user aborts as failures; rotation retries are
now visible in the rotation log at all (previously failures on a
rotated-to account were never logged, which is why account 2's death
was invisible in the support log).
- file-probe: the mpeg-ts signature required only a single leading 0x47
byte, so GIFs and "G"-prefixed text classified as video and would have
qualified junk for pool-wide alternate uploads; it now requires TS
sync-byte periodicity (0x47 at offsets 0/188/376) and the probe head
read grew from 64 to 512 bytes to make that check possible. GIF gets
its own non-video signature.
Tests: 9 new/reworked across suspect-reject-alternates.test.js (alternate
walk, no blacklist, memo short-circuit, account-error marking, abort
labeling, probe gating) and byse-reject-recovery.test.js (suspect polls +
recovers, empty poll throws suspectReject, Duplicate still fast-fails,
probe-confirmed non-video skips the poll) plus a TS/GIF probe regression
test. Full suite 276 green.