diff --git a/app/api/douban/image/route.ts b/app/api/douban/image/route.ts index a51deef..2a8bc70 100644 --- a/app/api/douban/image/route.ts +++ b/app/api/douban/image/route.ts @@ -2,6 +2,40 @@ import { NextResponse } from 'next/server'; export const runtime = 'edge'; +const REQUEST_HEADERS = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + Accept: 'image/jpeg,image/png,image/gif,*/*;q=0.8', + Referer: 'https://movie.douban.com/', +}; + +// 豆瓣的 imgN.doubanio.com 互为镜像,但各自解析到不同线路。部分线路(例如 img1 +// 的国内 IP)在境外主机上不可达,fetch 会直接抛错而不是返回状态码,海报因此变成 +// 500。同一路径换个镜像通常就能取到,所以失败时按顺序回退。 +const DOUBAN_IMAGE_HOSTS = ['img9.doubanio.com', 'img3.doubanio.com', 'img2.doubanio.com']; + +function buildCandidates(rawUrl: string): string[] { + const candidates = [rawUrl]; + + try { + const parsed = new URL(rawUrl); + if (!/^img\d+\.doubanio\.com$/.test(parsed.hostname)) { + return candidates; + } + + for (const host of DOUBAN_IMAGE_HOSTS) { + if (host === parsed.hostname) continue; + const alternate = new URL(parsed.toString()); + alternate.hostname = host; + candidates.push(alternate.toString()); + } + } catch { + // 非法 URL 留给 fetch 去报错 + } + + return candidates; +} + export async function GET(request: Request) { const { searchParams } = new URL(request.url); const imageUrl = searchParams.get('url'); @@ -10,51 +44,43 @@ export async function GET(request: Request) { return NextResponse.json({ error: 'Missing image URL' }, { status: 400 }); } - try { - const imageResponse = await fetch(imageUrl, { - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', - Accept: 'image/jpeg,image/png,image/gif,*/*;q=0.8', - Referer: 'https://movie.douban.com/', - }, - }); + let lastStatus = 502; + let lastError = 'Error fetching image'; + + for (const candidate of buildCandidates(imageUrl)) { + let imageResponse: Response; + + try { + imageResponse = await fetch(candidate, { headers: REQUEST_HEADERS }); + } catch { + // 线路不可达,换下一个镜像 + continue; + } if (!imageResponse.ok) { - return NextResponse.json( - { error: imageResponse.statusText }, - { status: imageResponse.status } - ); + lastStatus = imageResponse.status; + lastError = imageResponse.statusText || 'Error fetching image'; + continue; } - const contentType = imageResponse.headers.get('content-type'); - if (!imageResponse.body) { - return NextResponse.json( - { error: 'Image response has no body' }, - { status: 500 } - ); + lastStatus = 500; + lastError = 'Image response has no body'; + continue; } - // 创建响应头 const headers = new Headers(); + const contentType = imageResponse.headers.get('content-type'); if (contentType) { headers.set('Content-Type', contentType); } - - // 设置缓存头 headers.set('Cache-Control', 'public, max-age=15720000, s-maxage=15720000'); - // 直接返回图片流 - // @ts-ignore return new Response(imageResponse.body, { status: 200, headers, }); - } catch (error) { - return NextResponse.json( - { error: 'Error fetching image' }, - { status: 500 } - ); } + + return NextResponse.json({ error: lastError }, { status: lastStatus }); }