mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
117 lines
4.1 KiB
TypeScript
117 lines
4.1 KiB
TypeScript
import type { ProviderAttempt, ProviderId, UploadAttemptStage } from './types';
|
|
import { uploadToCatbox } from '../utils/catbox-utils';
|
|
|
|
/** Attempt metadata inferred or parsed from plugin rejection. Used when plugins throw plain errors. */
|
|
function parseAttemptMetadata(errorMessage: string): {
|
|
stage: UploadAttemptStage;
|
|
matchedSelectors?: string[];
|
|
} {
|
|
const msg = errorMessage.toLowerCase();
|
|
if (msg.includes('blocked') || msg.includes('captcha') || msg.includes('challenge')) {
|
|
return { stage: 'blocked' };
|
|
}
|
|
if (msg.includes('no file input') || msg.includes('file input')) {
|
|
const tried = errorMessage.match(/Tried:\s*(.+)$/)?.[1];
|
|
const selectors = tried
|
|
? tried
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)
|
|
: undefined;
|
|
return { stage: 'file_input', matchedSelectors: selectors };
|
|
}
|
|
if (msg.includes('no submit') || msg.includes('submit button')) {
|
|
const tried = errorMessage.match(/Tried:\s*(.+)$/)?.[1];
|
|
const selectors = tried
|
|
? tried
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)
|
|
: undefined;
|
|
return { stage: 'submit', matchedSelectors: selectors };
|
|
}
|
|
if (msg.includes('timeout') || msg.includes('upload timeout')) {
|
|
return { stage: 'timeout' };
|
|
}
|
|
if (msg.includes('page load failed')) {
|
|
return { stage: 'page_load' };
|
|
}
|
|
return { stage: 'unknown' };
|
|
}
|
|
|
|
function resolveElectronFilePath(file: File): string | null {
|
|
const fileWithPath = file as File & { path?: string };
|
|
if (typeof fileWithPath.path === 'string' && fileWithPath.path.length > 0) {
|
|
return fileWithPath.path;
|
|
}
|
|
|
|
const getPathForFile = window.electronApi?.getPathForFile;
|
|
if (typeof getPathForFile === 'function') {
|
|
const resolvedPath = getPathForFile(file);
|
|
if (typeof resolvedPath === 'string' && resolvedPath.length > 0) {
|
|
return resolvedPath;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Uploads a file via a single provider. Catbox uses the web API;
|
|
* imgur/imgbb use Electron automation when available.
|
|
*/
|
|
async function uploadViaProvider(provider: ProviderId, file: File): Promise<string> {
|
|
if (provider === 'catbox') return uploadToCatbox(file);
|
|
if (provider === 'imgur' || provider === 'imgbb') {
|
|
const fn = typeof window !== 'undefined' && window.electronApi?.automateUploadMedia;
|
|
if (fn) {
|
|
const filePath = resolveElectronFilePath(file);
|
|
if (!filePath) throw new Error('File path required for Electron automation');
|
|
const { url } = await fn({ provider, filePath });
|
|
return url;
|
|
}
|
|
throw new Error(`Provider ${provider} requires Electron (automateUploadMedia not available)`);
|
|
}
|
|
throw new Error(`Unsupported provider: ${provider}`);
|
|
}
|
|
|
|
/** Rejection shape for plugin errors that include structured metadata (future Electron/Android) */
|
|
interface PluginRejectionMeta {
|
|
stage?: UploadAttemptStage;
|
|
matchedSelectors?: string[];
|
|
}
|
|
|
|
/**
|
|
* Orchestrates Electron upload: tries each provider in order, returns URL on first success.
|
|
* Collects attempt errors with deterministic metadata (provider, stage, elapsedMs, matchedSelectors).
|
|
* Throws with attempts if all fail.
|
|
*/
|
|
export async function orchestrateElectronUpload(file: File, providerOrder: ProviderId[]): Promise<string> {
|
|
const attempts: ProviderAttempt[] = [];
|
|
|
|
for (const provider of providerOrder) {
|
|
const start = Date.now();
|
|
try {
|
|
const url = await uploadViaProvider(provider, file);
|
|
return url;
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
const elapsedMs = Date.now() - start;
|
|
const meta = err && typeof err === 'object' && 'stage' in err ? (err as PluginRejectionMeta) : null;
|
|
const parsed = parseAttemptMetadata(msg);
|
|
attempts.push({
|
|
provider,
|
|
success: false,
|
|
error: msg,
|
|
stage: meta?.stage ?? parsed.stage,
|
|
elapsedMs,
|
|
matchedSelectors: meta?.matchedSelectors ?? parsed.matchedSelectors,
|
|
});
|
|
}
|
|
}
|
|
|
|
const err = new Error('All providers failed') as Error & { attempts: ProviderAttempt[] };
|
|
err.attempts = attempts;
|
|
throw err;
|
|
}
|