/* eslint-disable @typescript-eslint/no-explicit-any */ import { HttpsProxyAgent } from 'https-proxy-agent'; import nodeFetch from 'node-fetch'; import { NextResponse } from 'next/server'; import { getConfig } from '@/lib/config'; export const runtime = 'nodejs'; function isCloudflareEnvironment(): boolean { return ( process.env.CF_PAGES === '1' || process.env.BUILD_TARGET === 'cloudflare' ); } function normalizeBaseUrl(baseUrl: string): string { return baseUrl.trim().replace(/\/+$/, ''); } function applyBangumiImageBaseUrl( imageUrl: string, imageBaseUrl?: string ): string { const normalizedBaseUrl = normalizeBaseUrl(imageBaseUrl || ''); if (!normalizedBaseUrl) { return imageUrl; } if (imageUrl.startsWith(`${normalizedBaseUrl}/`)) { return imageUrl; } return `${normalizedBaseUrl}/${imageUrl}`; } function isBangumiImageUrl(url: string): boolean { try { const hostname = new URL(url).hostname.toLowerCase(); return ( hostname === 'lain.bgm.tv' || hostname === 'r.bgm.tv' || hostname === 'bangumi.lol' || hostname.endsWith('.bgm.tv') || hostname.endsWith('.bangumi.tv') || hostname.endsWith('.bangumi.lol') ); } catch { return false; } } async function fetchImage( imageUrl: string, options?: { source?: string } ): Promise { const isBangumiImage = options?.source === 'bangumi' || isBangumiImageUrl(imageUrl); const 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/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', Referer: isBangumiImage ? 'https://bgm.tv/' : 'https://movie.douban.com/', }; const config = isBangumiImage ? await getConfig() : null; const targetUrl = isBangumiImage ? applyBangumiImageBaseUrl(imageUrl, config?.SiteConfig.BangumiImageBaseUrl) : imageUrl; if (!isBangumiImage || isCloudflareEnvironment()) { return fetch(targetUrl, { headers, signal: AbortSignal.timeout(15000) }); } const proxy = config?.SiteConfig.BangumiProxy?.trim(); const fetchOptions: any = { headers, signal: AbortSignal.timeout(proxy ? 30000 : 15000), }; if (proxy) { fetchOptions.agent = new HttpsProxyAgent(proxy, { timeout: 30000, keepAlive: false, }); } return nodeFetch(targetUrl, fetchOptions) as unknown as Promise; } // OrionTV 兼容接口 export async function GET(request: Request) { const { searchParams } = new URL(request.url); const imageUrl = searchParams.get('url'); const source = searchParams.get('source') || undefined; if (!imageUrl) { return NextResponse.json({ error: 'Missing image URL' }, { status: 400 }); } try { const imageResponse = await fetchImage(imageUrl, { source }); if (!imageResponse.ok) { return NextResponse.json( { error: imageResponse.statusText }, { status: imageResponse.status } ); } const contentType = imageResponse.headers.get('content-type'); if (!imageResponse.body) { return NextResponse.json( { error: 'Image response has no body' }, { status: 500 } ); } // 创建响应头 const headers = new Headers(); if (contentType) { headers.set('Content-Type', contentType); } // 设置缓存头(可选) headers.set('Cache-Control', 'public, max-age=15720000, s-maxage=15720000'); // 缓存半年 headers.set('CDN-Cache-Control', 'public, s-maxage=15720000'); headers.set('Vercel-CDN-Cache-Control', 'public, s-maxage=15720000'); headers.set('Netlify-Vary', 'query'); // 直接返回图片流 return new Response(imageResponse.body, { status: 200, headers, }); } catch (error) { console.error('图片代理请求失败:', error); return NextResponse.json( { error: 'Error fetching image' }, { status: 500 } ); } }