Fix duplicate VOE and Vidmoly recovery claims

Share account-scoped recovery claims with login uploader instances and serialize same-title recovery windows. Reserve direct and recovered file codes consistently, reject reused identities, and cover concurrent singleton recovery plus distinct-code and parallel-success cases.
This commit is contained in:
Sucukdeluxe
2026-08-13 22:30:27 +02:00
parent acb43d982e
commit c209abdbbc
5 changed files with 323 additions and 32 deletions
+178 -6
View File
@@ -5,11 +5,14 @@ const os = require('node:os');
const path = require('node:path');
const hosters = require('../lib/hosters');
const VoeUploader = require('../lib/voe-upload');
const VidmolyUploader = require('../lib/vidmoly-upload');
const originalUploadFile = hosters.uploadFile;
const originalPrefetchBaseline = hosters.prefetchBaseline;
let tempRoot;
let firstPath;
let secondPath;
let distinctPath;
let UploadManager;
before(() => {
@@ -20,8 +23,10 @@ before(() => {
fs.mkdirSync(secondDir);
firstPath = path.join(firstDir, 'Shared Episode.mkv');
secondPath = path.join(secondDir, 'shared-episode.mp4');
distinctPath = path.join(secondDir, 'different-title.mkv');
fs.writeFileSync(firstPath, Buffer.alloc(1024, 1));
fs.writeFileSync(secondPath, Buffer.alloc(1024, 2));
fs.writeFileSync(distinctPath, Buffer.alloc(1024, 3));
});
after(() => {
@@ -31,16 +36,16 @@ after(() => {
fs.rmSync(tempRoot, { recursive: true, force: true });
});
function loadManager(uploadFile) {
function loadManager(uploadFile = originalUploadFile) {
hosters.uploadFile = uploadFile;
hosters.prefetchBaseline = async () => new Set();
delete require.cache[require.resolve('../lib/upload-manager')];
UploadManager = require('../lib/upload-manager');
}
function settings(parallelCount) {
function settings(hoster, parallelCount) {
return {
'byse.sx': {
[hoster]: {
retries: 0,
parallelCount,
maxSpeedKbs: 0,
@@ -60,6 +65,29 @@ async function runBatch(manager, tasks) {
return summary;
}
async function withUploaderMethods(Uploader, upload, operation) {
const originalLogin = Uploader.prototype.login;
const originalUpload = Uploader.prototype.upload;
Uploader.prototype.login = async function () {};
Uploader.prototype.upload = upload;
try {
return await operation();
} finally {
Uploader.prototype.login = originalLogin;
Uploader.prototype.upload = originalUpload;
}
}
function waitFor(promise, timeoutMs, message) {
let timer;
return Promise.race([
promise,
new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(message)), timeoutMs);
})
]).finally(() => clearTimeout(timer));
}
test('a batch shares recovery claims across normalized same-name jobs', async () => {
let unsafeCalls = 0;
loadManager(async (hoster, file, apiKey, onProgress, signal, throttle, options) => {
@@ -81,7 +109,7 @@ test('a batch shares recovery claims across normalized same-name jobs', async ()
error.hosterTransient = true;
throw error;
});
const manager = new UploadManager(settings(2));
const manager = new UploadManager(settings('byse.sx', 2));
const summary = await runBatch(manager, [
{ jobId: 'same-name-a', file: firstPath, hoster: 'byse.sx', apiKey: 'ACCOUNT_KEY' },
@@ -107,7 +135,7 @@ test('recovery claims stay isolated between accounts', async () => {
download_url: 'https://byse.sx/d/SHARED_REMOTE_CODE'
};
});
const manager = new UploadManager(settings(2));
const manager = new UploadManager(settings('byse.sx', 2));
const summary = await runBatch(manager, [
{ jobId: 'account-a', file: firstPath, hoster: 'byse.sx', apiKey: 'ACCOUNT_A' },
@@ -133,7 +161,7 @@ test('normalized same-name recovery sections never overlap', async () => {
download_url: `https://byse.sx/d/SERIAL_${sequence}`
};
});
const manager = new UploadManager(settings(2));
const manager = new UploadManager(settings('byse.sx', 2));
const summary = await runBatch(manager, [
{ jobId: 'serialized-a', file: firstPath, hoster: 'byse.sx', apiKey: 'ACCOUNT_KEY' },
@@ -143,3 +171,147 @@ test('normalized same-name recovery sections never overlap', async () => {
assert.equal(summary.succeeded, 2);
assert.equal(maximumActive, 1);
});
for (const scenario of [
{
label: 'VOE',
hoster: 'voe.sx',
Uploader: VoeUploader,
sharedCode: 'SHAREDVOE01',
distinctCodes: ['VOEDISTINCT1', 'VOEDISTINCT2'],
buildResult(uploader, code) {
return uploader._buildUrls(code);
}
},
{
label: 'Vidmoly',
hoster: 'vidmoly.me',
Uploader: VidmolyUploader,
sharedCode: 'SHAREDVID001',
distinctCodes: ['VIDDISTINCT1', 'VIDDISTINCT2'],
buildResult(uploader, code) {
return uploader._buildUrlsFromCode(code);
}
}
]) {
test(`${scenario.label} uploader instances reject a duplicate direct remote code for same-name sources`, async () => {
await withUploaderMethods(
scenario.Uploader,
async function () {
await new Promise(resolve => setImmediate(resolve));
return scenario.buildResult(this, scenario.sharedCode);
},
async () => {
loadManager();
const manager = new UploadManager(settings(scenario.hoster, 2));
const summary = await runBatch(manager, [
{
jobId: `${scenario.label}-same-name-a`,
file: firstPath,
hoster: scenario.hoster,
accountId: 'LOGIN_ACCOUNT',
username: 'account@example.test',
password: 'password'
},
{
jobId: `${scenario.label}-same-name-b`,
file: secondPath,
hoster: scenario.hoster,
accountId: 'LOGIN_ACCOUNT',
username: 'account@example.test',
password: 'password'
}
]);
assert.equal(summary.succeeded, 1);
assert.equal(summary.failed, 1);
}
);
});
test(`${scenario.label} uploader instances isolate direct remote code claims between accounts`, async () => {
await withUploaderMethods(
scenario.Uploader,
async function () {
return scenario.buildResult(this, scenario.sharedCode);
},
async () => {
loadManager();
const manager = new UploadManager(settings(scenario.hoster, 2));
const summary = await runBatch(manager, [
{
jobId: `${scenario.label}-account-a`,
file: firstPath,
hoster: scenario.hoster,
accountId: 'LOGIN_ACCOUNT_A',
username: 'account-a@example.test',
password: 'password'
},
{
jobId: `${scenario.label}-account-b`,
file: secondPath,
hoster: scenario.hoster,
accountId: 'LOGIN_ACCOUNT_B',
username: 'account-b@example.test',
password: 'password'
}
]);
assert.equal(summary.succeeded, 2);
assert.equal(summary.failed, 0);
}
);
});
test(`${scenario.label} uploader instances preserve parallel success for distinct remote identities`, async () => {
let active = 0;
let maximumActive = 0;
let started = 0;
let releaseBoth;
const bothStarted = new Promise(resolve => {
releaseBoth = resolve;
});
await withUploaderMethods(
scenario.Uploader,
async function (filePath) {
active++;
started++;
maximumActive = Math.max(maximumActive, active);
if (started === 2) releaseBoth();
try {
await waitFor(bothStarted, 500, 'Distinct uploads did not overlap');
const code = filePath === firstPath ? scenario.distinctCodes[0] : scenario.distinctCodes[1];
return scenario.buildResult(this, code);
} finally {
active--;
}
},
async () => {
loadManager();
const manager = new UploadManager(settings(scenario.hoster, 2));
const summary = await runBatch(manager, [
{
jobId: `${scenario.label}-distinct-a`,
file: firstPath,
hoster: scenario.hoster,
accountId: 'LOGIN_ACCOUNT',
username: 'account@example.test',
password: 'password'
},
{
jobId: `${scenario.label}-distinct-b`,
file: distinctPath,
hoster: scenario.hoster,
accountId: 'LOGIN_ACCOUNT',
username: 'account@example.test',
password: 'password'
}
]);
assert.equal(summary.succeeded, 2);
assert.equal(summary.failed, 0);
assert.equal(maximumActive, 2);
}
);
});
}