TMDB Cloudflare Worker代理

このコミットが含まれているのは:
Harold
2026-06-06 15:07:35 +08:00
コミット 27afd6646a
4個のファイルの変更790行の追加0行の削除
+287
ファイルの表示
@@ -0,0 +1,287 @@
# TMDB Cloudflare Worker Proxy
一个基于 Cloudflare Worker 的 TMDB API 与图片代理服务。
支持:
* TMDB API 代理
* TMDB 图片代理
* 隐藏 API Key
* Cloudflare Edge Cache
* KV 二级缓存
* 多 API Key 轮换
* 自定义域名
---
# 文件说明
项目包含两个版本:
| 文件 | 说明 | 适合场景 |
| -------- | --- | --------------------------- |
| index.js | 轻量版 | 个人使用、小规模部署 |
| tmdb.js | 生产版 | ATV-Player、影视TV、TvBox 等公开服务 |
---
## index.js(轻量版)
特点:
* 单文件
* 无 KV 依赖
* API 代理
* 图片代理
* Edge Cache
* API Key 隐藏
适合:
* 自用
* 测试
* 小规模用户
部署时直接复制到 Cloudflare Worker 即可。
---
## tmdb.js(生产版)
特点:
* API 代理
* 图片代理
* API Key 隐藏
* Cloudflare Edge Cache
* KV 二级缓存
* 多 API Key 自动轮换
* IP 限流
* CORS
* 热门接口缓存
适合:
* ATV-Player
* TvBox
* 影视TV
* 公共 API 服务
推荐使用 Wrangler 部署。
---
# 获取 TMDB API Key
注册:
https://www.themoviedb.org/
创建 API Key
https://www.themoviedb.org/settings/api
---
# 部署方式
## 方案一:Dashboard(推荐新手)
### 创建 Worker
Cloudflare Dashboard
Workers & Pages
Create Worker
---
### 使用轻量版
复制:
```text
index.js
```
内容到 Worker。
---
### 添加环境变量
Settings
Variables
新增:
```text
TMDB_KEYS
```
值:
```text
your_tmdb_api_key
```
部署即可。
---
## 方案二:Wrangler(推荐生产环境)
### 安装
```bash
npm install -g wrangler
```
---
### 登录
```bash
wrangler login
```
---
### 创建 KV
```bash
wrangler kv namespace create TMDB_CACHE
```
记录返回的 Namespace ID。
---
### wrangler.toml
```toml
name = "tmdb-proxy"
main = "tmdb.js"
compatibility_date = "2026-06-06"
[[kv_namespaces]]
binding = "TMDB_CACHE"
id = "YOUR_KV_NAMESPACE_ID"
```
---
### 配置 API Key
```bash
wrangler secret put TMDB_KEYS
```
输入:
```text
key1,key2,key3
```
支持多个 Key。
---
### 本地运行
```bash
wrangler dev
```
---
### 部署
```bash
wrangler deploy
```
---
# API 示例
请求:
```text
https://tmdb.example.com/3/movie/550
```
实际访问:
```text
https://api.themoviedb.org/3/movie/550
```
并自动附加 API Key。
---
# 图片示例
请求:
```text
https://tmdb.example.com/t/p/w500/abc.jpg
```
实际访问:
```text
https://image.tmdb.org/t/p/w500/abc.jpg
```
---
# ATV-Player 配置
```text
TMDB API URL:
https://tmdb.example.com
TMDB Image URL:
https://tmdb.example.com
```
---
# 缓存策略
轻量版:
| 类型 | 缓存 |
| --- | --- |
| API | 1小时 |
| 图片 | 30天 |
生产版:
| 类型 | 缓存 |
| ---------- | ---- |
| Edge Cache | 1小时 |
| KV Cache | 24小时 |
| 图片 | 30天 |
---
# 推荐选择
个人使用:
```text
index.js
```
公开服务:
```text
tmdb.js
```
如果预计用户超过 1000 人,推荐直接使用生产版。
+117
ファイルの表示
@@ -0,0 +1,117 @@
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// 只允许 GET / HEAD 缓存
const cacheable =
request.method === "GET" ||
request.method === "HEAD";
const cache = caches.default;
if (cacheable) {
const cached = await cache.match(request);
if (cached) {
const headers = new Headers(cached.headers);
headers.set("X-TMDB-Cache", "HIT");
return new Response(cached.body, {
status: cached.status,
headers
});
}
}
let targetUrl;
let cacheTTL;
// ==========================
// API
// https://tmdb.xxx.com/3/movie/550
// ==========================
if (
url.pathname.startsWith("/3/") ||
url.pathname.startsWith("/4/")
) {
targetUrl =
"https://api.themoviedb.org" +
url.pathname +
url.search;
cacheTTL = 3600;
}
// ==========================
// 图片
// https://tmdb.xxx.com/t/p/w500/xxx.jpg
// ==========================
else if (
url.pathname.startsWith("/t/p/")
) {
targetUrl =
"https://image.tmdb.org" +
url.pathname;
cacheTTL = 2592000; // 30天
}
else {
return new Response(
JSON.stringify({
service: "TMDB Proxy",
api: "/3/*",
image: "/t/p/*"
}),
{
headers: {
"content-type": "application/json"
}
}
);
}
const upstream = await fetch(targetUrl, {
method: request.method,
headers: request.headers
});
const headers = new Headers(upstream.headers);
headers.set(
"Cache-Control",
`public, s-maxage=${cacheTTL}`
);
headers.set(
"Access-Control-Allow-Origin",
"*"
);
headers.set(
"X-TMDB-Cache",
"MISS"
);
const response = new Response(
upstream.body,
{
status: upstream.status,
statusText: upstream.statusText,
headers
}
);
if (
cacheable &&
upstream.ok
) {
ctx.waitUntil(
cache.put(
request,
response.clone()
)
);
}
return response;
}
};
+378
ファイルの表示
@@ -0,0 +1,378 @@
const API_CACHE_TTL = 3600; // 1小时
const IMAGE_CACHE_TTL = 2592000; // 30天
const KV_CACHE_TTL = 86400; // 1天
const RATE_LIMIT = 300; // 每分钟
export default {
async fetch(request, env, ctx) {
try {
return await handle(request, env, ctx);
} catch (e) {
return json(
{
error: e.message,
},
500
);
}
}
};
async function handle(request, env, ctx) {
const url = new URL(request.url);
// -------------------
// CORS
// -------------------
if (request.method === "OPTIONS") {
return new Response(null, {
headers: corsHeaders()
});
}
// -------------------
// Rate Limit
// -------------------
const ip =
request.headers.get("CF-Connecting-IP") ||
"unknown";
const rlKey =
`rl:${ip}:${Math.floor(Date.now()/60000)}`;
if (env.TMDB_CACHE) {
const count =
parseInt(
await env.TMDB_CACHE.get(rlKey) || "0"
);
if (count > RATE_LIMIT) {
return json(
{
error: "rate limit exceeded"
},
429
);
}
ctx.waitUntil(
env.TMDB_CACHE.put(
rlKey,
String(count + 1),
{
expirationTtl: 120
}
)
);
}
// -------------------
// Routing
// -------------------
if (
url.pathname.startsWith("/t/p/")
) {
return imageProxy(
request,
url,
ctx
);
}
return apiProxy(
request,
url,
env,
ctx
);
}
async function apiProxy(
request,
url,
env,
ctx
) {
const cache = caches.default;
const cacheKey =
new Request(
request.url,
request
);
let cached =
await cache.match(cacheKey);
if (cached) {
return addCacheHeader(
cached,
"EDGE-HIT"
);
}
const kvKey =
`api:${url.pathname}${url.search}`;
if (env.TMDB_CACHE) {
const kvData =
await env.TMDB_CACHE.get(
kvKey
);
if (kvData) {
return new Response(
kvData,
{
headers: {
"content-type":
"application/json",
...corsHeaders(),
"X-TMDB-Cache":
"KV-HIT"
}
}
);
}
}
const keys =
(env.TMDB_KEYS || "")
.split(",")
.filter(Boolean);
if (!keys.length) {
throw new Error(
"TMDB_KEYS missing"
);
}
const key =
keys[
Math.floor(
Date.now()/60000
) % keys.length
];
const upstreamUrl =
new URL(
"https://api.themoviedb.org" +
url.pathname
);
url.searchParams.forEach(
(v, k) =>
upstreamUrl.searchParams.set(
k,
v
)
);
upstreamUrl.searchParams.set(
"api_key",
key
);
const upstream =
await fetch(
upstreamUrl.toString(),
{
headers: {
accept:
"application/json"
}
}
);
const text =
await upstream.text();
const headers =
new Headers();
headers.set(
"content-type",
"application/json"
);
headers.set(
"cache-control",
`public,max-age=${API_CACHE_TTL}`
);
Object.entries(
corsHeaders()
).forEach(
([k, v]) =>
headers.set(k, v)
);
headers.set(
"X-TMDB-Cache",
"MISS"
);
const response =
new Response(
text,
{
status:
upstream.status,
headers
}
);
if (upstream.ok) {
ctx.waitUntil(
cache.put(
cacheKey,
response.clone()
)
);
if (env.TMDB_CACHE) {
ctx.waitUntil(
env.TMDB_CACHE.put(
kvKey,
text,
{
expirationTtl:
KV_CACHE_TTL
}
)
);
}
}
return response;
}
async function imageProxy(
request,
url,
ctx
) {
const cache =
caches.default;
const cached =
await cache.match(
request
);
if (cached) {
return addCacheHeader(
cached,
"EDGE-HIT"
);
}
const upstream =
await fetch(
"https://image.tmdb.org" +
url.pathname,
{
headers: request.headers
}
);
const headers =
new Headers(
upstream.headers
);
headers.set(
"cache-control",
`public,max-age=${IMAGE_CACHE_TTL}`
);
headers.set(
"Access-Control-Allow-Origin",
"*"
);
headers.set(
"X-TMDB-Cache",
"MISS"
);
const response =
new Response(
upstream.body,
{
status:
upstream.status,
headers
}
);
if (upstream.ok) {
ctx.waitUntil(
cache.put(
request,
response.clone()
)
);
}
return response;
}
function addCacheHeader(
response,
value
) {
const headers =
new Headers(
response.headers
);
headers.set(
"X-TMDB-Cache",
value
);
return new Response(
response.body,
{
status:
response.status,
headers
}
);
}
function corsHeaders() {
return {
"Access-Control-Allow-Origin":
"*",
"Access-Control-Allow-Methods":
"GET,HEAD,OPTIONS",
"Access-Control-Allow-Headers":
"*"
};
}
function json(
data,
status = 200
) {
return new Response(
JSON.stringify(data),
{
status,
headers: {
"content-type":
"application/json",
...corsHeaders()
}
}
);
}
+8
ファイルの表示
@@ -0,0 +1,8 @@
name = "tmdb-proxy"
[[kv_namespaces]]
binding = "TMDB_CACHE"
id = "YOUR_KV_ID"
[vars]
TMDB_KEYS = "key1,key2,key3"