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
+29 -13
View File
@@ -14,8 +14,9 @@ const RESULT_POLL_DELAY_MS = 2000;
* XFileSharing-based upload for Vidmoly (login + form upload)
*/
class VidmolyUploader {
constructor() {
constructor(recoveryClaim = null) {
this.cookies = new Map();
this.recoveryClaim = recoveryClaim;
}
_cookieHeader() {
@@ -352,6 +353,7 @@ class VidmolyUploader {
try {
return this._parseUploadResult(resultHtml);
} catch (primaryErr) {
if (primaryErr && primaryErr.remoteIdentityClaimed === true) throw primaryErr;
if (baselineCodes) {
const fallback = await this._resolveUploadedFileFromVmApi(fileName, baselineCodes, signal);
if (fallback) return fallback;
@@ -372,9 +374,21 @@ class VidmolyUploader {
.replace(/[^a-z0-9]+/g, '');
}
_buildUrlsFromCode(fileCode) {
_buildUrlsFromCode(fileCode, phase = 'upload-result') {
const code = String(fileCode || '').trim();
if (!code) return null;
if (this.recoveryClaim
&& typeof this.recoveryClaim.reserve === 'function'
&& !this.recoveryClaim.reserve(code)) {
const error = createTransportError('Vidmoly Upload-Ergebnis ist bereits einem anderen Upload zugeordnet', {
phase,
endpoint: BASE_URL,
retryable: true,
hosterTransient: true
});
error.remoteIdentityClaimed = true;
throw error;
}
return {
download_url: `${BASE_URL}/w/${code}`,
@@ -473,14 +487,18 @@ class VidmolyUploader {
const withCode = files.filter((f) => f && typeof f.file_code === 'string' && f.file_code.trim());
const newFiles = withCode.filter((f) => !baselineCodes.has(f.file_code.trim()));
const matches = newFiles.filter((file) => {
const title = this._normalizeTitle(file.full_title || file.title_txt || '');
return expectedTitle && title === expectedTitle;
});
const matches = newFiles
.filter((file) => {
const title = this._normalizeTitle(file.full_title || file.title_txt || '');
return expectedTitle && title === expectedTitle;
})
.filter((file) => !this.recoveryClaim
|| typeof this.recoveryClaim.has !== 'function'
|| !this.recoveryClaim.has(file.file_code.trim()));
if (matches.length > 1) return null;
if (matches.length === 1) {
return this._buildUrlsFromCode(matches[0].file_code);
return this._buildUrlsFromCode(matches[0].file_code, 'recovery-poll');
}
if (attempt < RESULT_POLL_ATTEMPTS - 1) {
@@ -577,12 +595,10 @@ class VidmolyUploader {
if (codeInPage) file_code = codeInPage[1];
}
// Build URLs from file_code
if (file_code && !download_url) {
download_url = `${BASE_URL}/w/${file_code}`;
}
if (file_code && !embed_url) {
embed_url = `${BASE_URL}/embed-${file_code}.html`;
if (file_code) {
const urls = this._buildUrlsFromCode(file_code);
if (!download_url) download_url = urls.download_url;
if (!embed_url) embed_url = urls.embed_url;
}
if (!download_url && !file_code) {