Files
5chan/src/lib/media-hosting/upload-orchestrator.ts
T

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;
}