Home
avatar

whohh

利用Edgeone边缘函数反代TMDB

前言

这是一个利用腾讯Edgeone边缘函数进行代理tmdb接口的项目
前者有利用Vercel代理tmdb接口的仓库,但是呢我的Emby库根本不够用,因为Vercel每月有100GB流量限制,对于我庞大的库来说完全不够,而且每天都在入库新的媒体

该项目可以直接代理TMDB的API以及图片接口,可直接供神医助手以及其他的可以替换TMDB地址的项目

Vercel

本文将把 https://github.com/qqcomeup/CF-TMDB-Proxy- 的 Cloudflare Worker 代码改写为 Edgeone 边缘函数代码。Edgeone 在国内使用速度上都更快。

部署步骤

1. 准备 Edgeone 账号

先获取 Edgeone 免费名额,具体方式请移步 https://bing.com

2. 创建边缘函数

  1. 打开 Edgeone 控制台。
  2. 找到你的站点,进入站点概览。
  3. 点击 边缘函数函数管理新建函数创建 Hello World

EDGEONE

  1. 完成创建后点击“下一步”。

  2. 进入函数代码编辑页面。

EDGEONE

3. 复制并部署代码

以下是完整的边缘函数代码:

var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });

// EdgeOne 使用标准 addEventListener 监听 fetch 事件
addEventListener("fetch", (event) => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  const url = new URL(request.url);
  const pathname = url.pathname;
  const search = url.search;
  
  const corsHeaders = {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key",
    "Cross-Origin-Resource-Policy": "cross-origin"
  };

  function rewriteConfigImages(payload) {
    if (!payload || typeof payload !== "object" || !payload.images)
      return payload;
    const origin = url.origin.replace(/\/$/, "");
    const proxyBase = `${origin}/t/p/`;
    payload.images.base_url = proxyBase;
    payload.images.secure_base_url = proxyBase;
    return payload;
  }
  __name(rewriteConfigImages, "rewriteConfigImages");

  if (request.method === "OPTIONS") {
    return new Response(null, { status: 200, headers: corsHeaders });
  }

  const API_KEY = request.headers.get("X-API-Key") || url.searchParams.get("api_key") || url.searchParams.get("key");
  const userAgent = request.headers.get("User-Agent") || "";
  
  // EdgeOne 获取客户端真实 IP 的标准请求头是 X-Forwarded-For 或通过 request.eo 获取
  const clientIP = request.headers.get("X-Forwarded-For")?.split(',')[0].trim() || "unknown";
  
  // EdgeOne 对应的地理位置信息在 request.eo.geo 中
  const country = request.eo?.geo?.countryCode || "unknown";

  const suspiciousUA = ["curl", "wget", "python", "scrapy", "spider"];
  const isSuspicious = suspiciousUA.some((ua) => userAgent.toLowerCase().includes(ua));
  
  if (userAgent.toLowerCase().includes("bot") && !userAgent.includes("googlebot") || isSuspicious && !userAgent.includes("Mozilla")) {
    return new Response(getFake404HTML(), { status: 404, headers: { "Content-Type": "text/html", ...corsHeaders } });
  }

  const blockedCountries = [];
  if (blockedCountries.includes(country)) {
    return new Response(getFake404HTML(), { status: 404, headers: { "Content-Type": "text/html", ...corsHeaders } });
  }

  if (pathname === "/admin/status" && API_KEY && API_KEY.length === 32) {
    return new Response(JSON.stringify({
      status: "active",
      version: "2.0.0",
      endpoints: { images: "/t/p/{size}/{path}", api: "/3/{endpoint}" },
      client_info: { ip: clientIP, country, ua: userAgent.substring(0, 50) },
      security: { api_key_provided: true, request_secure: true },
      performance: { cache_enabled: true, compression: true },
      timestamp: (new Date()).toISOString()
    }), {
      headers: { "Content-Type": "application/json", ...corsHeaders }
    });
  }

  if (pathname === "/health" || pathname === "/ping") {
    return new Response(JSON.stringify({
      status: "ok",
      timestamp: (new Date()).toISOString(),
      uptime: "active"
    }), {
      headers: { "Content-Type": "application/json", ...corsHeaders }
    });
  }

  if (pathname === "/" || pathname === "") {
    return new Response(getFake404HTML(), {
      status: 404,
      headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
    });
  }

  // 1. 图片代理部分
  if (pathname.startsWith("/t/p/")) {
    try {
      const imageUrl = `https://image.tmdb.org${pathname}`;
      
      // 注意:EdgeOne 默认遵循源站的缓存策略。
      // 如果要强制在 EdgeOne 节点缓存图片,推荐在返回的 Response 中设置 Cache-Control 强缓存,
      // 或者在 EdgeOne 控制台的“规则引擎”中为 /t/p/* 配置缓存时间(7天)。
      const response = await fetch(imageUrl, {
        headers: {
          "User-Agent": "Mozilla/5.0 (compatible; TMDB-Proxy/1.0)",
          "Accept": "image/*"
        }
      });

      if (!response.ok) {
        return new Response(getFake404HTML(), {
          status: 404,
          headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
        });
      }

      // 组装返回给客户端的图片
      return new Response(response.body, {
        status: response.status,
        headers: {
          "Content-Type": response.headers.get("Content-Type") || "image/jpeg",
          // 在此处加入强缓存响应头,EdgeOne 节点和浏览器均会缓存该图片 7 天
          "Cache-Control": "public, max-age=604800, immutable",
          "ETag": response.headers.get("ETag") || "",
          "Last-Modified": response.headers.get("Last-Modified") || "",
          "Content-Length": response.headers.get("Content-Length") || "",
          "Vary": "Accept-Encoding",
          ...corsHeaders
        }
      });
    } catch (error) {
      return new Response(getFake404HTML(), {
        status: 404,
        headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
      });
    }
  }

  // 2. API 代理部分
  if (pathname.startsWith("/3/")) {
    if (!API_KEY) {
      return new Response(getFake404HTML(), {
        status: 404,
        headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
      });
    }
    try {
      let apiUrl = `https://api.tmdb.org${pathname}${search}`;
      if (!search.includes("api_key=")) {
        const separator = search ? "&" : "?";
        apiUrl += `${separator}api_key=${API_KEY}`;
      }

      // 【核心修复】:显式构建标准的、干净的请求头,避免携带原请求中带有你域名的 Host
      const forwardHeaders = new Headers();
      forwardHeaders.set("Host", "api.tmdb.org");
      forwardHeaders.set("Accept", "application/json");
      forwardHeaders.set("User-Agent", request.headers.get("User-Agent") || "Mozilla/5.0");
      
      // 注意:暂时移除过多的 Accept-Encoding,让 EdgeOne 自动处理压缩,防止 text() 解析乱码
      if (request.headers.has("Accept-Language")) {
        forwardHeaders.set("Accept-Language", request.headers.get("Accept-Language"));
      }

      const response = await fetch(apiUrl, {
        method: request.method,
        headers: forwardHeaders
      });

      // 检查源站是否返回错误
      if (!response.ok) {
        return new Response(JSON.stringify({ error: `TMDB origin error: ${response.status}` }), {
          status: response.status,
          headers: { "Content-Type": "application/json", ...corsHeaders }
        });
      }

      let responseText = await response.text();
      
      const cacheTime = pathname.includes("configuration") ? 3600 : (
        pathname.includes("search") ? 300 : (
          pathname.includes("popular") ? 1800 : 600
        )
      );

      if (pathname.startsWith("/3/configuration")) {
        try {
          const json = JSON.parse(responseText);
          responseText = JSON.stringify(rewriteConfigImages(json));
        } catch (err) {
          // 打印错误日志便于在 EdgeOne 控制台调试
          console.error("Failed to parse configuration JSON:", err);
        }
      }

      // 移除可能冲突的底层编码头,让 EdgeOne 统一重新打包
      const responseHeaders = new Headers(corsHeaders);
      responseHeaders.set("Content-Type", "application/json; charset=utf-8");
      responseHeaders.set("Cache-Control", `public, max-age=${cacheTime}`);

      return new Response(responseText, {
        status: response.status,
        headers: responseHeaders
      });
    } catch (error) {
      // 捕捉具体错误返回,方便你排查究竟是 fetch 失败还是 JSON 解析失败
      return new Response(JSON.stringify({ 
        error: "Service temporarily unavailable", 
        details: error.message 
      }), {
        status: 503,
        headers: { "Content-Type": "application/json", ...corsHeaders }
      });
    }
  }

  return new Response(getFake404HTML(), {
    status: 404,
    headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
  });
}

function getFake404HTML() {
  return `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>404 Not Found</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { 
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: #f8f9fa; color: #212529; line-height: 1.6; min-height: 100vh;
            display: flex; align-items: center; justify-content: center;
        }
        .error-container { text-align: center; max-width: 600px; padding: 2rem; }
        .error-code { font-size: 8rem; font-weight: 300; color: #6c757d; margin-bottom: 1rem; line-height: 1; }
        .error-title { font-size: 2rem; font-weight: 400; color: #495057; margin-bottom: 1rem; }
        .error-message { font-size: 1.1rem; color: #6c757d; margin-bottom: 2rem; }
        .error-details {
            background: #e9ecef; border-radius: 8px; padding: 1rem; margin: 1.5rem 0;
            font-family: 'Courier New', monospace; font-size: 0.9rem; color: #495057; text-align: left;
        }
        .back-link {
            display: inline-block; padding: 0.75rem 1.5rem; background: #007bff; color: white;
            text-decoration: none; border-radius: 4px; transition: background-color 0.2s;
        }
        .back-link:hover { background: #0056b3; }
        .footer { margin-top: 3rem; font-size: 0.9rem; color: #adb5bd; }
        .server-info { margin-top: 1rem; font-size: 0.8rem; color: #ced4da; }
    </style>
</head>
<body>
    <div class="error-container">
        <div class="error-code">404</div>
        <h1 class="error-title">Page Not Found</h1>
        <p class="error-message">The requested resource could not be found on this server.</p>

        <div class="error-details">
            <strong>Error Details:</strong><br>
            \u2022 Request Method: GET<br>
            \u2022 Request URL: ${new Date().toISOString().split("T")[0]}<br>
            \u2022 Server: Tencent Cloud EdgeOne<br>
            \u2022 Timestamp: ${new Date().toISOString()}
        </div>

        <p style="color: #6c757d; margin: 1.5rem 0;">
            If you believe this is an error, please contact the site administrator.
        </p>

        <a href="javascript:history.back()" class="back-link">\u2190 Go Back</a>

        <div class="footer">
            <p>This page was generated automatically.</p>
            <div class="server-info">Server: EdgeOne Functions | Error Code: HTTP_404_NOT_FOUND</div>
        </div>
    </div>

    <script>
        console.log('%c\u{1F3AC} TMDB Proxy Service v2.0 (EdgeOne)', 'color: #007bff; font-size: 16px; font-weight: bold;');
        console.log('%cService Status: \u2705 Active (Enhanced)', 'color: #28a745;');
        console.log('%cEndpoints:', 'color: #6c757d;');
        console.log('  \u2022 Images: /t/p/{size}/{path} (7-day cache)');
        console.log('  \u2022 API: /3/{endpoint} (Smart cache 5min-1hr)');
        console.log('  \u2022 Health: /health, /ping');
        console.log('  \u2022 Admin: /admin/status (requires API key)');
        console.log('%cAPI Key Methods:', 'color: #17a2b8;');
        console.log('  \u2022 Header: X-API-Key: your_api_key');
        console.log('  \u2022 URL Param: ?api_key=your_api_key');
        console.log('  \u2022 URL Param: ?key=your_api_key');
        console.log('%cFeatures: Cache, Compression, Security, Geo-blocking', 'color: #28a745;');
        console.log('%c\u26A0\uFE0F Disguised as 404 for security', 'color: #ffc107;');

        window.testAPI = () => fetch('/3/configuration').then(r=>r.json()).then(console.log);
        window.testImage = () => { const i=new Image(); i.onload=()=>console.log('\u2705 Image OK'); i.onerror=()=>console.log('\u274C Image failed'); i.src='/t/p/w500/bcP7FtskwsNp1ikpMQJzDPjofP5.jpg'; };
        console.log('%cTest: testAPI() | testImage()', 'color: #17a2b8;');
    <\/script>
</body>
</html>`;
}
__name(getFake404HTML, "getFake404HTML");

这样就成功部署好了。

自定义域名与触发规则

新增自定义托管域名

前往 域名服务域名管理添加域名

EDGEONE

填写自定义域名、源站和缓存规则。

EDGEONE

添加触发规则

进入新创建的函数项目,找到 触发规则,点击 新增触发规则,保存即可。

EDGEONE

这个步骤相当于 Cloudflare Worker 的域名路由。

验证部署

访问你的域名后缀:

  • /health
  • /ping

若返回:

{"status":"ok","timestamp":"2026-06-02T12:26:39.808Z","uptime":"active"}

表示部署成功,服务正常运行。

TMDB API 文档: 官方文档
Cloudflare Workers 文档: 官方文档

edgeone tmdb 边缘函数