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:
+99
-224
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user