release: restore v2.1.19 baseline for v2.1.24
CI / verify (push) Has been cancelled

Restore the v2.1.19 application baseline and retain only the focused import preflight summary with duplicate, unavailable, destination, job, and size-limit visibility.
This commit is contained in:
Sucukdeluxe
2026-08-17 04:25:22 +02:00
parent 9a213a7395
commit d7c9f287e4
87 changed files with 2185 additions and 16125 deletions
+99 -224
View File
@@ -2,8 +2,6 @@ const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { request } = require('undici');
const { createTransportError, sanitizeRemoteText } = require('./hoster-transport-error');
const { normalizeRecoveryTitle } = require('./hosters');
const BASE_URL = 'https://vidmoly.me';
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
@@ -15,9 +13,8 @@ const RESULT_POLL_DELAY_MS = 2000;
* XFileSharing-based upload for Vidmoly (login + form upload)
*/
class VidmolyUploader {
constructor(recoveryClaim = null) {
constructor() {
this.cookies = new Map();
this.recoveryClaim = recoveryClaim;
}
_cookieHeader() {
@@ -133,51 +130,14 @@ class VidmolyUploader {
* removed. Returns an XFS-style session token + a transit-server URL.
*/
async getUploadParams() {
const endpoint = `${BASE_URL}/api/upload/config`;
let res;
try {
res = await this._fetch(endpoint);
} catch {
throw createTransportError('Vidmoly: Upload-Konfiguration konnte nicht geladen werden', {
phase: 'upload-config',
endpoint,
retryable: true,
transientNetwork: true
});
}
const res = await this._fetch(`${BASE_URL}/api/upload/config`);
const body = await res.text();
const contentType = res.headers && typeof res.headers.get === 'function'
? res.headers.get('content-type')
: null;
if (res.status < 200 || res.status >= 300) {
throw createTransportError('Vidmoly: Upload-Konfiguration konnte nicht geladen werden', {
phase: 'upload-config',
endpoint,
httpStatus: res.status,
contentType,
body,
retryable: res.status === 429 || res.status >= 500,
transientNetwork: res.status >= 500
});
}
let payload = null;
try { payload = JSON.parse(body); } catch {
throw createTransportError('Vidmoly: Upload-Konfiguration war kein JSON', {
phase: 'upload-config',
endpoint,
httpStatus: res.status,
contentType,
body
});
throw new Error('Vidmoly: /api/upload/config lieferte kein JSON — evtl. nicht eingeloggt?');
}
if (!payload || !payload.sess_id || !payload.upload_url) {
throw createTransportError('Vidmoly: Upload-Konfiguration war unvollständig', {
phase: 'upload-config',
endpoint,
httpStatus: res.status,
contentType,
body
});
throw new Error('Vidmoly: /api/upload/config unvollständig (sess_id/upload_url fehlt)');
}
return {
uploadUrl: payload.upload_url,
@@ -194,14 +154,7 @@ class VidmolyUploader {
async upload(filePath, onProgress, signal, throttle) {
const fileName = path.basename(filePath);
const fileSize = fs.statSync(filePath).size;
let baselineCodes = null;
let baselineError = null;
try {
baselineCodes = await this._captureVmFileCodes();
} catch (err) {
if (signal && signal.aborted) throw err;
baselineError = err;
}
const baselineCodes = await this._captureVmFileCodes();
const { uploadUrl, params, fileFieldName } = await this.getUploadParams();
@@ -258,34 +211,21 @@ class VidmolyUploader {
const targetUrl = uploadUrl + (uploadUrl.includes('?') ? '&' : '?') + 'X-Progress-ID=' + progressId;
// Browsers don't send vidmoly.me cookies across origins, so we don't either.
let uploadResponse;
try {
uploadResponse = await request(targetUrl, {
method: 'POST',
body: generate(),
signal,
headers: {
'User-Agent': USER_AGENT,
'Accept': '*/*',
'Origin': BASE_URL,
'Referer': `${BASE_URL}/`,
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'Content-Length': String(totalSize)
},
headersTimeout: UPLOAD_TIMEOUT,
bodyTimeout: UPLOAD_TIMEOUT
});
} catch (err) {
const error = signal && signal.aborted ? err : createTransportError('Vidmoly Upload konnte nicht übertragen werden', {
phase: 'upload-request',
endpoint: targetUrl,
retryable: true,
transientNetwork: true
});
throw this._markRemoteCommitUncertain(error);
}
const { body, statusCode, headers } = uploadResponse;
const { body, statusCode, headers } = await request(targetUrl, {
method: 'POST',
body: generate(),
signal,
headers: {
'User-Agent': USER_AGENT,
'Accept': '*/*',
'Origin': BASE_URL,
'Referer': `${BASE_URL}/`,
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'Content-Length': String(totalSize)
},
headersTimeout: UPLOAD_TIMEOUT,
bodyTimeout: UPLOAD_TIMEOUT
});
this._parseCookiesFromHeaders(headers || {});
@@ -296,34 +236,13 @@ class VidmolyUploader {
// Always drain the original body to prevent connection leak
try { await body.text(); } catch {}
if (location) {
try {
const resultRes = await this._fetch(new URL(location, uploadUrl).href);
resultHtml = await resultRes.text();
} catch (err) {
throw this._markRemoteCommitUncertain(err);
}
const resultRes = await this._fetch(new URL(location, uploadUrl).href);
resultHtml = await resultRes.text();
} else {
resultHtml = '';
}
} else {
try {
resultHtml = await body.text();
} catch (err) {
throw this._markRemoteCommitUncertain(err);
}
}
if (statusCode >= 400) {
const error = createTransportError('Vidmoly Upload fehlgeschlagen', {
phase: 'upload-response',
endpoint: targetUrl,
httpStatus: statusCode,
contentType: headers && headers['content-type'],
body: resultHtml,
retryable: statusCode === 429 || statusCode >= 500,
transientNetwork: statusCode >= 500
});
throw statusCode >= 500 ? this._markRemoteCommitUncertain(error) : error;
resultHtml = await body.text();
}
// Try JSON first. The current transit server returns
@@ -348,69 +267,43 @@ class VidmolyUploader {
if (urls) return urls;
}
if (json.status && !/ok/i.test(json.status) && json.msg) {
throw createTransportError(`Vidmoly Upload abgelehnt: ${sanitizeRemoteText(json.msg)}`, {
phase: 'upload-result',
endpoint: targetUrl,
httpStatus: statusCode,
contentType: 'application/json',
body: resultHtml
});
throw new Error(`Vidmoly Upload abgelehnt: ${json.msg}`);
}
} catch (err) {
if (err && err.diagnostic) throw err;
if (err && /Vidmoly Upload abgelehnt/.test(err.message)) throw err;
}
try {
return this._parseUploadResult(resultHtml);
} catch (primaryErr) {
if (primaryErr && primaryErr.remoteIdentityClaimed === true) throw primaryErr;
if (baselineCodes) {
try {
const fallback = await this._resolveUploadedFileFromVmApi(fileName, baselineCodes, signal);
if (fallback) return fallback;
} catch (err) {
throw this._markRemoteCommitUncertain(err);
}
}
if (baselineError) {
baselineError.hosterTransient = true;
throw this._markRemoteCommitUncertain(baselineError);
}
throw this._markRemoteCommitUncertain(primaryErr);
const fallback = await this._resolveUploadedFileFromVmApi(fileName, baselineCodes, signal);
if (fallback) return fallback;
throw primaryErr;
}
}
_normalizeTitle(value) {
return normalizeRecoveryTitle(value);
return String(value || '')
.toLowerCase()
.normalize('NFKD')
.replace(/[^a-z0-9]+/g, '');
}
_markRemoteCommitUncertain(error) {
if (this.recoveryClaim && typeof this.recoveryClaim.markUncertain === 'function') {
return this.recoveryClaim.markUncertain(error);
}
const uncertainError = error && typeof error === 'object'
? error
: new Error('Vidmoly Upload-Ergebnis ist unsicher');
uncertainError.remoteCommitUncertain = true;
uncertainError.hosterTransient = true;
return uncertainError;
_scoreVmCandidate(file, expectedTitle) {
if (!file || !file.file_code) return -1;
if (!expectedTitle) return 0;
const title = this._normalizeTitle(file.full_title || file.title_txt || '');
if (!title) return -1;
if (title === expectedTitle) return 120;
if (title.startsWith(expectedTitle) || expectedTitle.startsWith(title)) return 90;
if (title.includes(expectedTitle) || expectedTitle.includes(title)) return 70;
return 0;
}
_buildUrlsFromCode(fileCode, phase = 'upload-result') {
_buildUrlsFromCode(fileCode) {
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 this._markRemoteCommitUncertain(error);
}
return {
download_url: `${BASE_URL}/w/${code}`,
@@ -420,15 +313,19 @@ class VidmolyUploader {
}
async _captureVmFileCodes() {
const files = await this._fetchVmList('recovery-baseline');
return new Set(
files
.map((f) => String(f.file_code || '').trim())
.filter(Boolean)
);
try {
const files = await this._fetchVmList();
return new Set(
files
.map((f) => String(f.file_code || '').trim())
.filter(Boolean)
);
} catch {
return new Set();
}
}
async _fetchVmList(phase = 'recovery-poll') {
async _fetchVmList() {
const params = new URLSearchParams({
op: 'vm',
api: 'list',
@@ -439,46 +336,14 @@ class VidmolyUploader {
fld_id: '0'
});
const endpoint = `${BASE_URL}/?${params.toString()}`;
let res;
try {
res = await this._fetch(endpoint);
} catch {
throw createTransportError('Vidmoly: Dateiliste konnte nicht geladen werden', {
phase,
endpoint,
retryable: true,
transientNetwork: true
});
}
const res = await this._fetch(`${BASE_URL}/?${params.toString()}`);
const body = await res.text();
const contentType = res.headers && typeof res.headers.get === 'function'
? res.headers.get('content-type')
: null;
if (res.status < 200 || res.status >= 300) {
throw createTransportError('Vidmoly: Dateiliste konnte nicht geladen werden', {
phase,
endpoint,
httpStatus: res.status,
contentType,
body,
retryable: res.status === 429 || res.status >= 500,
transientNetwork: res.status >= 500
});
}
let payload;
try {
payload = JSON.parse(body);
} catch {
throw createTransportError('Vidmoly: Dateiliste war kein JSON', {
phase,
endpoint,
httpStatus: res.status,
contentType,
body
});
throw new Error('Vidmoly VM API lieferte kein JSON');
}
if (!payload || !Array.isArray(payload.files)) return [];
@@ -486,10 +351,7 @@ class VidmolyUploader {
}
async _resolveUploadedFileFromVmApi(fileName, baselineCodes, signal) {
if (!(baselineCodes instanceof Set)) return null;
const expectedTitle = this._normalizeTitle(path.parse(fileName).name);
let lastPollError = null;
let successfulPoll = false;
for (let attempt = 0; attempt < RESULT_POLL_ATTEMPTS; attempt++) {
if (signal && signal.aborted) {
@@ -500,27 +362,46 @@ class VidmolyUploader {
let files = [];
try {
files = await this._fetchVmList('recovery-poll');
successfulPoll = true;
} catch (err) {
if (err && err.name === 'AbortError') throw err;
lastPollError = err;
files = await this._fetchVmList();
} catch {
files = [];
}
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;
})
.filter((file) => !this.recoveryClaim
|| typeof this.recoveryClaim.has !== 'function'
|| !this.recoveryClaim.has(file.file_code.trim()));
const newFiles = withCode.filter((f) => !baselineCodes.has(f.file_code));
if (matches.length > 1) return null;
if (matches.length === 1) {
return this._buildUrlsFromCode(matches[0].file_code, 'recovery-poll');
if (newFiles.length > 0) {
let best = null;
let bestScore = -1;
for (const file of newFiles) {
const score = this._scoreVmCandidate(file, expectedTitle);
if (score > bestScore) {
bestScore = score;
best = file;
}
}
if (best && bestScore > 0) {
return this._buildUrlsFromCode(best.file_code);
}
}
if (expectedTitle) {
let bestMatch = null;
let bestScore = -1;
for (const file of withCode) {
const score = this._scoreVmCandidate(file, expectedTitle);
if (score > bestScore) {
bestScore = score;
bestMatch = file;
}
}
if (bestMatch && bestScore >= 90) {
return this._buildUrlsFromCode(bestMatch.file_code);
}
}
if (attempt < RESULT_POLL_ATTEMPTS - 1) {
@@ -528,7 +409,6 @@ class VidmolyUploader {
}
}
if (!successfulPoll && lastPollError) throw lastPollError;
return null;
}
@@ -617,23 +497,18 @@ class VidmolyUploader {
if (codeInPage) file_code = codeInPage[1];
}
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;
// 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 (!download_url && !file_code) {
const errMatch = html.match(/class=["']err["'][^>]*>([^<]+)/i);
const errMsg = errMatch ? errMatch[1].trim() : 'Kein Download-Link gefunden';
throw createTransportError(`Vidmoly Upload-Ergebnis: ${sanitizeRemoteText(errMsg)}`, {
phase: 'upload-result',
endpoint: BASE_URL,
contentType: 'text/html',
body: html,
hosterTransient: true,
retryable: true
});
throw new Error(`Vidmoly Upload-Ergebnis: ${errMsg}`);
}
return { download_url, embed_url, file_code };