chore(media-upload): remove Postimages provider

Postimages cannot support WebView automation (site blocks automated uploads).
Imgur works reliably. Keep only Catbox and Imgur as supported providers.
This commit is contained in:
plebeius
2026-02-28 19:13:43 +08:00
parent 274a6fb4b1
commit 7fd3e3fd45
28 changed files with 389 additions and 727 deletions
@@ -21,7 +21,7 @@ const mockSetUploadMode = vi.fn();
const mockSetPreferredProvider = vi.fn();
const uploadModeRef = vi.hoisted(() => ({ value: 'random' as 'random' | 'preferred' | 'none' }));
const preferredProviderRef = vi.hoisted(() => ({
value: 'catbox' as 'catbox' | 'imgur' | 'postimages',
value: 'catbox' as 'catbox' | 'imgur',
}));
vi.mock('../../../../stores/use-media-hosting-store', async (importOriginal) => {
const mod = await importOriginal<typeof import('../../../../stores/use-media-hosting-store')>();
+2 -2
View File
@@ -35,12 +35,12 @@ vi.mock('../../lib/media-hosting/provider-order', () => ({
getProviderOrder: vi.fn((opts: { mode: string; preferredProvider: string; runtime: string }) => {
if (opts.mode === 'none') return [];
if (opts.runtime === 'web') return opts.preferredProvider === 'catbox' ? ['catbox'] : [];
return opts.mode === 'preferred' ? [opts.preferredProvider] : ['catbox', 'imgur', 'postimages'];
return opts.mode === 'preferred' ? [opts.preferredProvider] : ['catbox', 'imgur'];
}),
}));
const uploadModeRef = vi.hoisted(() => ({ value: 'random' as 'random' | 'preferred' | 'none' }));
const preferredProviderRef = vi.hoisted(() => ({ value: 'catbox' as 'catbox' | 'imgur' | 'postimages' }));
const preferredProviderRef = vi.hoisted(() => ({ value: 'catbox' as 'catbox' | 'imgur' }));
vi.mock('../../stores/use-media-hosting-store', () => ({
default: (selector: (s: { uploadMode: string; preferredProvider: string }) => unknown) =>
selector({ uploadMode: uploadModeRef.value, preferredProvider: preferredProviderRef.value }),
+1 -1
View File
@@ -26,7 +26,7 @@ const ANDROID_STAGE_MAP: Record<string, UploadAttemptStage> = {
const FILE_SELECTION_CANCELLED_ERROR = 'File selection cancelled';
const VALID_PROVIDERS: ProviderId[] = ['catbox', 'imgur', 'postimages'];
const VALID_PROVIDERS: ProviderId[] = ['catbox', 'imgur'];
/** Raw attempt shape from Android plugin rejection payload */
interface RawAttempt {
@@ -6,25 +6,24 @@ describe('provider-order', () => {
it('returns single-element array with preferred provider', () => {
expect(getPreferredOrder('catbox')).toEqual(['catbox']);
expect(getPreferredOrder('imgur')).toEqual(['imgur']);
expect(getPreferredOrder('postimages')).toEqual(['postimages']);
});
});
describe('getRandomOrder', () => {
it('returns shuffled copy (Fisher-Yates) with default rng', () => {
const providers = ['catbox', 'imgur', 'postimages'] as const;
const providers = ['catbox', 'imgur'] as const;
const result = getRandomOrder(providers);
expect(result).toHaveLength(3);
expect([...result].sort()).toEqual(['catbox', 'imgur', 'postimages']);
expect(result).toHaveLength(2);
expect([...result].sort()).toEqual(['catbox', 'imgur']);
expect(result).not.toBe(providers);
});
it('uses provided rng for deterministic shuffle', () => {
const providers = ['catbox', 'imgur', 'postimages'] as const;
const providers = ['catbox', 'imgur'] as const;
const rng = vi.fn().mockReturnValue(0.5);
const result = getRandomOrder(providers, rng);
expect(rng).toHaveBeenCalled();
expect(result).toHaveLength(3);
expect(result).toHaveLength(2);
});
it('handles empty array', () => {
@@ -80,8 +79,8 @@ describe('provider-order', () => {
preferredProvider: 'catbox',
runtime: 'electron',
});
expect(order).toHaveLength(3);
expect([...order].sort()).toEqual(['catbox', 'imgur', 'postimages']);
expect(order).toHaveLength(2);
expect([...order].sort()).toEqual(['catbox', 'imgur']);
});
it('filters by runtime for random mode', () => {
@@ -56,14 +56,14 @@ describe('orchestrateElectronUpload', () => {
const file = new File(['z'], 'z.png', { type: 'image/png' });
try {
await orchestrateElectronUpload(file, ['postimages']);
await orchestrateElectronUpload(file, ['imgur']);
throw new Error('Expected orchestrateElectronUpload to throw');
} catch (error) {
const typedError = error as Error & {
attempts?: Array<{ provider: string; error?: string; elapsedMs?: number; stage?: string }>;
};
expect(typedError.message).toBe('All providers failed');
expect(typedError.attempts?.[0]?.provider).toBe('postimages');
expect(typedError.attempts?.[0]?.provider).toBe('imgur');
expect(typedError.attempts?.[0]?.error).toContain('File path required for Electron automation');
expect(typedError.attempts?.[0]?.elapsedMs).toBeGreaterThanOrEqual(0);
expect(typedError.attempts?.[0]?.stage).toBeDefined();
@@ -115,21 +115,19 @@ describe('orchestrateElectronUpload', () => {
it('parses timeout stage when upload or URL extraction times out', async () => {
const electronApi = createElectronApiMock();
electronApi.automateUploadMedia = vi
.fn()
.mockRejectedValue(new Error('Upload timeout or no direct URL extracted for postimages (elapsed: 45000ms, timeout: 45000ms)'));
electronApi.automateUploadMedia = vi.fn().mockRejectedValue(new Error('Upload timeout or no direct URL extracted for imgur (elapsed: 45000ms, timeout: 45000ms)'));
window.electronApi = electronApi;
const file = new File(['x'], 'x.png', { type: 'image/png' });
try {
await orchestrateElectronUpload(file, ['postimages']);
await orchestrateElectronUpload(file, ['imgur']);
throw new Error('Expected orchestrateElectronUpload to throw');
} catch (error) {
const typedError = error as Error & {
attempts?: Array<{ provider: string; stage?: string }>;
};
expect(typedError.attempts?.[0]?.provider).toBe('postimages');
expect(typedError.attempts?.[0]?.provider).toBe('imgur');
expect(typedError.attempts?.[0]?.stage).toBe('timeout');
}
});
-6
View File
@@ -22,12 +22,6 @@ export const MEDIA_HOSTING_PROVIDERS: readonly ProviderDefinition[] = [
homepageUrl: 'https://imgur.com',
supportedRuntimes: ['electron', 'android'],
},
{
id: 'postimages',
label: 'Postimages',
homepageUrl: 'https://postimages.org',
supportedRuntimes: ['electron', 'android'],
},
] as const;
/** Provider IDs for ordering */
+1 -1
View File
@@ -1,5 +1,5 @@
/** Supported media hosting provider identifiers */
export type ProviderId = 'catbox' | 'imgur' | 'postimages';
export type ProviderId = 'catbox' | 'imgur';
/** User-facing upload mode */
export type UploadMode = 'random' | 'preferred' | 'none';
+2 -2
View File
@@ -58,11 +58,11 @@ function resolveElectronFilePath(file: File): string | null {
/**
* Uploads a file via a single provider. Catbox uses the web API;
* imgur/postimages use Electron automation when available.
* imgur uses Electron automation when available.
*/
async function uploadViaProvider(provider: ProviderId, file: File): Promise<string> {
if (provider === 'catbox') return uploadToCatbox(file);
if (provider === 'imgur' || provider === 'postimages') {
if (provider === 'imgur') {
const fn = typeof window !== 'undefined' && window.electronApi?.automateUploadMedia;
if (fn) {
const filePath = resolveElectronFilePath(file);
@@ -19,7 +19,7 @@ describe('useMediaHostingStore', () => {
it('exports MEDIA_HOSTING_PROVIDERS with ids, labels, homepage URLs, runtime metadata', () => {
expect(MEDIA_HOSTING_PROVIDERS).toBeDefined();
expect(MEDIA_HOSTING_PROVIDERS.length).toBe(3);
expect(MEDIA_HOSTING_PROVIDERS.length).toBe(2);
const catbox = MEDIA_HOSTING_PROVIDERS.find((p) => p.id === 'catbox');
expect(catbox).toEqual({
id: 'catbox',