Self-hosted streaming / 部署文档

把影视、直播
放进一台服务器。

冬瓜TV 是一个自托管影视聚合站。Docker 一条命令启动,后台添加采集站与直播源,TMDb 资料、PanSou 网盘搜索和 Android 客户端都在同一套服务里。

Docker Compose TMDb PanSou M3U Live
冬瓜TV 桌面首页,横版剧照、TMDb Logo 与推荐内容
冬瓜TV 手机首页,无字竖版海报
向下阅读

界面预览

截图来自实际界面,后台示例使用虚构站点。首页会按屏幕宽度选择横版剧照或竖版海报,详情页使用大背景、海报缩略图和 TMDb 透明 Logo。点开任意图片可查看原图。

冬瓜TV 首页 TMDb 推荐
TMDb 影视详情
TMDb 播放平台与相似推荐
移动端首页 TMDb
移动端搜索结果
移动端 TMDb 详情
后台采集站管理
后台导入 TVBox 配置和苹果CMS接口
后台生成 Android 客户端
后台 TMDb 凭据设置
后台使用教程

部署

方式 A:Docker Compose(推荐)

会同时启动「主站」和「PanSou 网盘搜索」两个容器,适合绝大多数人。

建议部署在能稳定访问 TMDb 的海外服务器。国内机器可能打不开 TMDb 的 API 和图片, 需要额外代理,首页推荐和海报会不稳定。
git clone https://github.com/Minerchu/dongguaTV.git
cd dongguaTV
# 可选:复制配置模板,至少修改管理员密码
cp .env.example .env
docker compose up -d --build

启动后访问:

  • 主站:http://服务器IP:3000
  • 后台:http://服务器IP:3000/admin.html

采集站和后台设置保存在 ./data/db.json,docker compose down 或重建容器都不会丢。

页面地址可以直接分享

搜索、影视详情和播放页现在都有独立地址,浏览器前进/后退和刷新都能恢复:

  • /:首页
  • /search?wd=庆余年:聚合搜索结果
  • /title/movie/671、/title/tv/1399:电影或剧集详情
  • /watch?name=片名&site=站点key&id=影片ID:播放页

Docker 的 HOST_PORT 只影响网址里是否带端口。不想显示 :3100 时,用 Nginx / Caddy 把域名代理到该端口即可,见下一节。

方式 B:单容器镜像(All-in-One)

主站和 PanSou 打进同一个镜像,只跑一个容器。适合不方便用 compose 的面板。

docker build -f Dockerfile.allinone -t dongguatv:allinone .

docker run -d --name dongguatv \
  -p 3000:3000 \
  -v dongguatv-data:/app/data \
  -e ADMIN_PASSWORD=改成你自己的密码 \
  dongguatv:allinone

PanSou 已内置,只在容器内部监听 8888,不需要也不建议把 8888 开到公网。

构建时需要拉取 ghcr.io/fish2018/pansou。如果服务器拉不动, 请先用代理或镜像加速拉取,或者直接用方式 A 的 compose。

方式 C:不用 Docker(Node.js)

npm install
cp .env.example .env
# 编辑 .env,至少填写 ADMIN_PASSWORD;TMDb 可以启动后在后台填
npm start

需要 Node.js 18 以上(推荐 20 / 22)。TMDb 凭据由服务端读取,不需要改前端。

需要准备的东西

项目是否必须说明
TMDb 凭据需要短 API Key 或长 Read Access Token 二选一,登录后台「系统设置」填写
采集站接口必须在后台自行添加,格式见第四节
PanSou可选网盘搜索;compose / all-in-one 已经内置

修改端口

先记住一句话:左边是宿主机端口,右边是容器端口。

ports:
  - "8080:3000"
#    ↑    ↑
# 浏览器访问 8080
#        容器里的程序仍然监听 3000

1. 只改访问端口(最常见)

推荐在 .env 里改 HOST_PORT,容器内部端口不用动:

HOST_PORT=8080
docker compose up -d

也可以直接改 docker-compose.yml 里冒号左边的数字:

ports:
  - "8080:3000"

之后访问 http://服务器IP:8080 即可,程序内部不用改。

2. 同时修改容器内部端口

如果 3000 被别的程序占用,或者你想让程序本身也监听 8080:

ports:
  - "8080:8080"
environment:
  - PORT=8080

用 docker run 时同理:

docker run -d --name dongguatv \
  -e PORT=8080 \
  -p 8080:8080 \
  dongguatv:allinone

3. PanSou 的端口(一般不用改)

Compose 和 All-in-One 都让 PanSou 只在容器内部通信, 不需要对公网开放端口,平时不用改。只有容器内部端口冲突, 或你确实想单独访问 PanSou 时才需要动它。

Compose 方式(改完要让主站指向新端口):

pansou:
  environment:
    - PORT=18888
# 同时把主站的 PANSOU_URL 改成 http://pansou:18888

All-in-One 方式(只改容器内部端口,不发布到宿主机):

docker run -d --name dongguatv \
  -p 3000:3000 \
  -e PANSOU_PORT=18888 \
  dongguatv:allinone
All-in-One 镜像会自动把 PANSOU_URL 指向容器内的 http://127.0.0.1:${PANSOU_PORT},你不用手动改。

4. 不用 Docker 时

编辑项目根目录的 .env:

PORT=8080

或者临时启动:

PORT=8080 node server.js

5. 域名和 HTTPS

用域名访问、去掉网址里的端口,需要做反向代理。Cloudflare、Nginx 和宝塔三种方式都在 第八节,按你现有的环境选一种即可。

首次使用

  1. 打开 http://服务器IP:端口/admin.html
  2. 输入管理员密码。默认是 admin
  3. 登录后先做三件事:改密码、填 TMDb 凭据、加采集站

填写 TMDb 凭据

打开后台「系统设置」,只粘贴其中一个凭据,点「保存 TMDb 配置」,再点「测试连接」确认可用。

  • API Key:大约 32 位字母数字,使用 TMDb 的 v3 接口。
  • API Read Access Token:以 eyJ 开头的很长一串,使用 Bearer 认证。
这两个是 TMDb 同时展示的两种凭据,只需要填一个。也可以直接把 TMDb 页面上的一整段文字粘进去, 系统会自动提取有效的 Key 或 Token,不需要手动加 tmdb、Bearer 或“密钥”这些字。 保存后后台只显示末尾四位;要更换就重新粘贴,要删除点「清除后台凭据」。后台填写的值优先于 .env。

修改管理员密码

在 docker-compose.yml 或 .env 里改:

ADMIN_PASSWORD=一个复杂密码
docker compose up -d
默认密码 admin 只能用于本地测试。部署到公网前必须改掉。

添加采集站

项目里不内置任何采集站,也不在 GitHub 上保存任何站点地址。 你需要自己找公开的 Maccms V10 JSON 接口,然后在后台添加。

1. 正确格式长什么样

不是网站首页,也不是播放页,而是接口地址,通常以这些形式结尾:

https://example.com/api.php/provide/vod
https://example.com/inc/apijson.php

2. 怎么找

在 Google 里搜索下面任意一个关键词组合:

  • "api.php/provide/vod"
  • inurl:"api.php/provide/vod"
  • maccms 采集接口
  • "inc/apijson.php"

也可以搜「影视采集站 分享」「Maccms 资源站 接口」等中文关键词。

3. 验证能不能用

把地址复制到浏览器,后面加上 ?ac=list&pg=1:

https://example.com/api.php/provide/vod?ac=list&pg=1

看到这样的 JSON 就说明格式正确:

{
  "code": 1,
  "msg": "数据列表",
  "list": [
    { "vod_id": 123, "vod_name": "示例电影", "vod_pic": "https://..." }
  ]
}

如果看到的是一整页网页、XML、404 或 500,就换一个。

4. 添加到后台

  1. 打开「采集站管理」
  2. 填站点名称和接口地址
  3. 点「测试并添加」,显示「连接正常」就成功了

5. 批量导入与查重

站点多的时候,打开左边的「导入采集源」,不用一条条手填。

  • TVBox 配置:每行粘贴一个配置地址,支持明文 JSON、Base64、图片隐写、2423/2324 加密和 urls 仓库。
  • 苹果CMS 接口:每行一个接口,可以只填地址,也可以写成「名称,地址」。
我的资源站,https://example.com/api.php/provide/vod
另一个站,https://example.org/inc/apijson.php

点「解析并查重」后会先出预览表。已存在的地址标灰,不能再选;新接口勾选后点「导入选中」即可。

TVBox 里的 csp_ 内置源和 Jar 源要靠播放器运行,网页后台无法直接调用,导入时会自动跳过并显示原因。 能导入的是配置里公开的 Maccms HTTP 接口。
添加后的地址只保存在服务器本地的 db.json(已在 .gitignore 中), 不要把自己的 db.json 上传到 GitHub,也不要截图公开接口地址。

6. 播放直连与手动切源

前台和安卓客户端会直接用采集站提供的 m3u8 地址播放,视频和分片都不会经过本站服务器,不占用服务器带宽。 播放速度取决于用户到源站之间的线路。

站内播放器也可以单独调用:/player/?url=你的m3u8地址&name=影片名。

当前线路播放失败时,播放器只提示手动切换,不再自动尝试其他线路。每条线路是否可用受源站接口、播放格式、m3u8 是否存在以及源站是否允许浏览器跨域访问影响。 遇到跨域或 403 时,换其他线路即可。

7. 搜索聚合与 PING 巡检

同一个片名和年份会合并成一张卡片,显示「X 站 / Y 条线路」,点进去再选站点。 服务每 10 分钟对启用的采集站做一次 HTTP PING,只看是否能连通和延迟;连续失败 3 次先冷却 5 分钟,之后自动重新参与搜索,不会永久关闭。

后台「采集站管理」能看到延迟、成功率和冷却剩余时间,也可以点「PING 巡检」立即检查;PING 正常不代表接口一定返回 m3u8。

8. 观看记录和收藏

浏览记录、收藏和继续观看保存在浏览器本地,不上传服务器,也不需要注册账号。 清除浏览器数据会一起清掉这些记录。

9. 自定义源适配器

内置采集只认 Maccms JSON。自建 JSON、字段名不同或需要特殊请求头的站点,可以用服务端适配器接入。 适配器是放在 data/adapters/ 里的 .js 文件,不是浏览器插件。

  1. 把适配器文件放进宿主机 data/adapters/。
  2. 后台「采集站管理」点「重载适配器」。
  3. 添加采集站时选择「自定义适配器」,填写基础地址和 JSON 配置。
mkdir -p data/adapters
cp examples/adapters/generic-json.js data/adapters/
# 然后回后台点「重载适配器」,不需要重启容器

通用示例支持配置 searchPath、queryParam、detailPath 和 testQuery。 适配器接口说明见仓库 examples/adapters/README.md。

适配器会在服务器上执行代码,只安装你信任的来源。安装后可以正常参与搜索、详情和 PING 巡检。

网盘搜索(PanSou)

PanSou 是一个独立的 Go 服务,负责搜索 Telegram 频道和网盘插件。 本项目的 Compose 和 All-in-One 镜像已经把它打包进去,不需要你单独部署。

不用重新搭建,也不用改 Go 代码。 方式 A 是一条 docker compose up -d 同时起两个容器; 方式 B 是同一个镜像里跑两个进程。两种方式第一次启动就可用。

Compose 方式

docker compose up -d 会同时启动 pansou 容器, 主站通过 PANSOU_URL=http://pansou:8888 自动连接,后台不用填地址。

All-in-One 方式

镜像内部会自动启动 PanSou,并设置好 PANSOU_URL,开箱即用。

使用已有的 PanSou

# 不启动自带的 pansou 服务,把地址改成你自己的
PANSOU_URL=http://你的地址:8888

也可以在后台「网盘搜索」页填写地址、Token、开关,点保存即可。

前台搜索页会按夸克、百度、阿里、UC 等网盘类型显示筛选按钮,复制链接和提取码都在结果卡片里。

访问不了 Telegram?

给 pansou 容器配置代理:

pansou:
  environment:
    - PROXY=socks5://host.docker.internal:7890
    - HTTPS_PROXY=http://host.docker.internal:7890
    - HTTP_PROXY=http://host.docker.internal:7890

检索结果太少?

PanSou 默认频道/插件有限,可以在 compose 里覆盖它自己的环境变量: CHANNELS、ENABLED_PLUGINS。具体取值见 PanSou 文档。

电视直播

后台「电视直播」可以添加 M3U / M3U8 订阅,前台首页会出现「直播」入口。频道列表、分组、搜索、收藏和 HLS 播放都在 /live。

添加直播源

  1. 登录后台,打开「电视直播」。
  2. 点「添加直播订阅」,填写一个名称和订阅地址。填整份频道列表,不是单个频道的播放地址。
  3. 点「测试」,能显示频道数和分组数后再点「保存直播设置」。
  4. 打开 /live 或首页的「直播」入口,选择频道播放。

订阅文件一般长这样:

#EXTM3U
#EXTINF:-1 tvg-logo="https://example.com/logo.png" group-title="央视",CCTV-1
https://example.com/live/cctv1.m3u8
#EXTINF:-1 group-title="卫视",湖南卫视
https://example.com/live/hunan.m3u8
当前支持浏览器可播放的 HTTP/HTTPS HLS、MP4 和 WebM。FLV、rtp://、udp:// 会被忽略。 播放优先直连源站,失败后才走服务器签名代理。订阅里带私人 Token 时,只会保存在服务器的 data/db.json,不会写进前端源码。

没有直播源时,后台可以添加多条订阅并单独启停。某一条订阅失效不会影响其他订阅。

Android 原生客户端

客户端使用 Kotlin + Jetpack Compose 编写,直接调用本站的 /api/search、/api/detail、/api/cloud/search 和 TMDb 同源接口, 播放器使用 Media3 / ExoPlayer。它不是网页套壳,也不需要 WebView。

使用标准 docker compose 部署时,Android SDK、Gradle、apktool、zipalign 和 apksigner 都已经包含在镜像里。站长不需要安装 Android Studio,也不需要在电脑上编译 APK。

生成客户端

  1. 先在「采集站管理」添加至少一个可用的 Maccms JSON 接口。
  2. 打开后台的「Android 客户端」页。
  3. 设置应用名称,上传正方形 PNG 图标;浏览器会自动裁剪成 512×512。
  4. 填写手机能访问的服务器地址,例如 https://tv.example.com。
  5. 点击「保存并生成 APK」。服务器会自动排队编译,通常需要 1–3 分钟。
  6. 状态变成「已生成」后,在后台或首页下载 APK。

服务器地址怎么填

  • 必须是手机实际能访问的地址,不要填 127.0.0.1、容器名或容器内部端口。
  • 已经配置域名和 HTTPS 时,填最终公网地址,例如 https://tv.example.com。
  • 局域网测试可以填 http://192.168.1.10:3000,手机和服务器要在同一网络。
  • 客户端内置地址不可用时,也可以在客户端「设置」页手动修改服务器地址。

扫码绑定

后台 Android 页面会显示服务器二维码。安装 APK 后,在首次连接页或「设置」里点「扫码绑定」, 扫码即可写入地址并测试连接,不需要在手机上手动输入。

二维码只带服务器地址,不带后台密码。修改服务器地址并保存后,二维码会自动更新。

更新和签名

data/android-client/
├── donggua-tv.apk      # 生成好的安装包
├── signing.jks         # 本站独立签名
└── signing.pass        # 签名密码

修改名称、图标、版本号或服务器地址后,保存会自动重新生成。 更新服务时必须保留 data/android-client,否则新 APK 会使用不同签名,手机无法覆盖安装。

如需排查失败原因,运行 docker compose logs -f dongguatv 查看构建日志。 All-in-One 镜像不包含 APK 编译组件;需要 Android 客户端时请使用标准 Dockerfile / Compose。

客户端「设置」页可以检查站点是否发布了新版本;发现新版本会直接打开 APK 下载地址。 播放页右上角可以把当前集交给系统里的其他播放器或投屏应用。

网站域名与反向代理

建议把服务部署在能稳定访问 TMDb 的海外服务器。中国大陆机器访问 TMDb 可能超时, 首页推荐、海报和演员资料会不完整。这和下面的域名反向代理是两件事:前者决定服务器能否访问 TMDb, 后者决定用户能否用域名和 HTTPS 打开你的网站。
先分清两个“反代”: 这一节代理的是冬瓜TV网站本身,目标是 http://127.0.0.1:3000; TMDb 反代的目标只有 api.themoviedb.org 和 image.tmdb.org, 不要把 3000 端口填进后台的「TMDb 反代根地址」。TMDb 专用配置见 第九节。

先确定源站地址

默认 Docker 对外端口是 3000,反代目标就是 http://127.0.0.1:3000。 如果你在第二节把 HOST_PORT 改成了 3100,下面配置里的 3000 也要一起换掉。先在服务器上确认能访问:

curl -I http://127.0.0.1:3000

域名服务商处添加一条 A 记录,指向服务器公网 IP。等解析生效后,从下面三种方案里只选一种。

正式环境建议只对外开放 80 和 443,不要长期把 3000 直接暴露在公网。反代必须原样转发 /api、/tmdb 和 /admin.html,不要加路径前缀,也不要开启静态缓存。

方案 A:Cloudflare 反向代理

适合不想手动续证书、希望顺带使用 CDN 和防护的人。注意:Cloudflare 的普通橙色云代理只从 80/443 回源,不能直接把橙色云指向服务器的 3000。源站需要先用 Nginx 或 Caddy 监听 443,或者改用 Cloudflare Tunnel。

  1. 在 Cloudflare 添加域名,DNS 里新建 A 记录,填写服务器 IP,先开启橙色云。
  2. 如果源站已经有有效证书,SSL/TLS 模式选 Full (strict); 源站暂时只有 HTTP 时只能选 Full 或 Flexible,但安全性较差,不推荐长期这样用。
  3. 在 SSL/TLS → Edge Certificates 打开 Always Use HTTPS。
  4. 源站按下面 Nginx 教程把 443 转发到 3000,或者使用 Tunnel。

不想开放公网端口时,在 Cloudflare Zero Trust → Networks → Tunnels 新建 Tunnel, 装好连接器后添加 Public Hostname,Service 填 http://127.0.0.1:3000。 临时测试也可以先运行:

cloudflared tunnel --url http://127.0.0.1:3000
Cloudflare 代理不适合承载很大的视频流量。个人使用一般够用;如果经常播放或流量较大, 建议把该域名改成灰云(仅 DNS),让 Nginx 直接承担播放请求和 TLS。

方案 B:Nginx 反向代理

Ubuntu / Debian 安装 Nginx 和 Certbot:

sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx

新建 /etc/nginx/sites-available/dongguatv,把域名和端口换成自己的:

server {
    listen 80;
    server_name tv.example.com;
    client_max_body_size 64m;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_buffering off;
        proxy_request_buffering off;
    }
}

启用配置并申请证书:

sudo ln -s /etc/nginx/sites-available/dongguatv /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d tv.example.com

之后访问 https://tv.example.com。如果 Nginx 本身也在 Docker 里, 把 127.0.0.1:3000 换成主站容器的服务名,例如 http://dongguatv:3000。

方案 C:宝塔面板反向代理

  1. 软件商店确认已安装 Nginx,然后在「网站」→「添加站点」添加域名,PHP 版本选纯静态,不创建数据库。
  2. 进入站点设置 →「SSL」,申请 Let's Encrypt 证书,开启「强制 HTTPS」。
  3. 打开「反向代理」→「添加反向代理」,目标 URL 填 http://127.0.0.1:3000,发送域名填 $host。
  4. 在反向代理配置中开启 WebSocket,关闭缓存,把超时时间设为 3600 秒。
  5. 保存后访问域名检查首页、后台、搜索和播放。站点目录不需要放任何文件。

如果宝塔生成的配置里没有下面几项,可在反向代理配置中补上;已经存在就不要重复添加:

proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;

反向代理完成后检查

  • 浏览器地址栏只显示 https://你的域名,不再带 :3000。
  • 后台能登录,搜索、详情和播放都正常;如果登录后反复跳回登录页,检查 X-Forwarded-Proto。
  • 播放中断时,优先检查 proxy_buffering off 和两个 3600s 超时。
  • 后台生成 Android 客户端时,服务器地址必须填最终域名,例如 https://tv.example.com,不能填 127.0.0.1。
项目还附带可选的 Caddy 配置。如果不想安装 Nginx,可以在 .env 填 DOMAIN=tv.example.com,再执行 docker compose -f docker-compose.yml -f docker-compose.https.yml up -d --build。 这种方式占用 80/443,不能和宝塔/Nginx 同时监听同一端口。

TMDb 功能

首页和详情页已经用到的功能

  • 首页推荐:本周趋势、华语剧集、日韩剧集、港台电影、正在热映、高分电影、今日播出。
  • 海报与 Logo:电脑和电视使用横版剧照,手机使用无字竖版海报;详情页叠加 TMDb 透明 Logo,没有 Logo 时自动回退文字标题。
  • 影视详情:评分、上映日期、片长、分级、类型、关键词、主创、演员和简介。
  • 系列作品:电影属于系列时,展示同系列其他影片,可直接继续查看。
  • 预告片:优先使用 YouTube 官方预告;新片没有预告时不会显示空按钮。
  • 在哪儿看:按浏览器地区显示 Netflix、Disney+ 等平台,也可手动切换国家/地区。
  • 相似推荐与找片源:点击推荐继续浏览;点“找片源”会带片名回到 Maccms 聚合搜索,同时搜索 PanSou 网盘资源。
TMDb 只提供影视资料、图片和推荐,不存储影视文件。实际播放仍来自你在后台添加的采集站。

TMDb 请求由本站服务端转发,浏览器只访问当前站点域名,凭据不会进入前端:

  • 前端 /tmdb/3/... → https://api.themoviedb.org/3/...
  • 海报 /tmdb/t/p/... → https://image.tmdb.org/t/p/...
  • TMDb 凭据只保存在服务端或 data/db.json,不会出现在网页源码里
这里说的内置代理只负责把 TMDb API 和图片请求转发出去,不等于给网站配置公网域名和 HTTPS。 用户访问你的网站仍需要按上一节配置 Cloudflare、Nginx 或宝塔。

请求链路:

浏览器 → https://你的域名/tmdb/... → 容器内 server.js → TMDb

服务器本身能访问 TMDb 时,不需要填反代。修改 Docker 对外端口也不会影响它, 因为前端始终请求当前网站地址下的 /tmdb/...。

注意:/tmdb 是容器内的服务端转发,不是 Cloudflare 反代。服务器在国内时, 容器本身仍然连不上 api.themoviedb.org,需要在后台填写下面的反代地址。

服务器在国内:反向代理 TMDb

这里不是反代网站,也不是反代 3000 端口。 需要转发的是 TMDb 的两类地址:https://api.themoviedb.org/3/... 和 https://image.tmdb.org/t/p/...。反代完成后,后台填的是这个代理的根地址, 例如 https://tmdb.example.com,不是 http://127.0.0.1:3000。

后台「系统设置 → TMDb 影视资料 → TMDb 网络线路」已经可以直接填写反代地址,保存后立即生效, 不需要改 .env,也不需要重建容器。

  1. 登录 Cloudflare Dashboard,进入 Workers & Pages,创建一个 Worker。
  2. 打开 Worker 的代码编辑器,把下面代码全部替换进去,然后点 Deploy。
  3. 访问 Worker 根地址,例如 https://tmdb-proxy.example.workers.dev, 看到 {"ok":true} 说明部署成功。
  4. 回到冬瓜TV 后台,把这个根地址填进「TMDb 反代根地址」,点「测试连接 → 保存 TMDb 设置」。

只填根地址,不要手动拼 /3 或 /t/p;系统会自动补齐。 如果 API 和图片使用不同的代理,再单独填写「图片反代地址」。


const API_ORIGIN = 'https://api.themoviedb.org';
const IMAGE_ORIGIN = 'https://image.tmdb.org';

function corsHeaders() {
    return {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
        'Access-Control-Allow-Headers': 'Authorization, Content-Type, Range',
        'Access-Control-Expose-Headers': 'Content-Length, Content-Range, Content-Type'
    };
}

function textResponse(body, status) {
    return new Response(body, {
        status: status || 200,
        headers: Object.assign({ 'Content-Type': 'text/plain; charset=utf-8' }, corsHeaders())
    });
}

export default {
    async fetch(request) {
        const incoming = new URL(request.url);

        if (request.method === 'OPTIONS') {
            return new Response(null, { status: 204, headers: corsHeaders() });
        }
        if (incoming.pathname === '/' || incoming.pathname === '/health') {
            return new Response(JSON.stringify({ ok: true, service: 'donggua-tmdb-proxy' }), {
                headers: Object.assign({ 'Content-Type': 'application/json; charset=utf-8' }, corsHeaders())
            });
        }
        if (request.method !== 'GET' && request.method !== 'HEAD') {
            return textResponse('Method Not Allowed', 405);
        }

        let origin = '';
        if (incoming.pathname.startsWith('/3/')) origin = API_ORIGIN;
        else if (incoming.pathname.startsWith('/t/p/')) origin = IMAGE_ORIGIN;
        else return textResponse('Not Found', 404);

        const headers = new Headers();
        const passHeaders = ['accept', 'accept-language', 'authorization', 'content-type', 'if-modified-since', 'if-none-match', 'range'];
        for (const name of passHeaders) {
            const value = request.headers.get(name);
            if (value) headers.set(name, value);
        }
        headers.set('user-agent', 'DongguaTV-TMDB-Proxy/1.0');

        const target = origin + incoming.pathname + incoming.search;
        const upstream = await fetch(target, {
            method: request.method,
            headers,
            redirect: 'follow',
            cf: {
                cacheEverything: true,
                cacheTtl: incoming.pathname.startsWith('/t/p/') ? 604800 : 300
            }
        });

        const responseHeaders = new Headers(upstream.headers);
        responseHeaders.delete('set-cookie');
        responseHeaders.set('Access-Control-Allow-Origin', '*');
        responseHeaders.set('Access-Control-Expose-Headers', 'Content-Length, Content-Range, Content-Type');
        responseHeaders.set('X-Donggua-TMDB-Proxy', '1');

        return new Response(request.method === 'HEAD' ? null : upstream.body, {
            status: upstream.status,
            statusText: upstream.statusText,
            headers: responseHeaders
        });
    }
};

完整文件也在仓库:examples/cloudflare-tmdb-worker.mjs。

方式 A:绑定自己的 Cloudflare 域名

如果 *.workers.dev 在当前网络下访问不了,可以在 Worker 的 Settings → Domains & Routes → Custom Domain 绑定一个已经接入 Cloudflare 的域名, 例如 tmdb.example.com。绑定成功后在后台填 https://tmdb.example.com。

方式 B:Nginx 反代 TMDb

准备一个只给 TMDb 用的域名,例如 tmdb.example.com,把 A 记录指向这台海外服务器。 先写 HTTP 配置,证书交给 Certbot 自动补全。新建 /etc/nginx/sites-available/tmdb-proxy:

server {
    listen 80;
    server_name tmdb.example.com;

    location = / {
        default_type application/json;
        return 200 '{"ok":true,"service":"donggua-tmdb-proxy"}';
    }

    location ^~ /3/ {
        proxy_pass https://api.themoviedb.org/3/;
        proxy_ssl_server_name on;
        proxy_set_header Host api.themoviedb.org;
        proxy_set_header Authorization $http_authorization;
        proxy_set_header Accept-Language $http_accept_language;
        proxy_set_header User-Agent DongguaTV-TMDB-Proxy/1.0;
        proxy_set_header Range $http_range;
        proxy_set_header If-None-Match $http_if_none_match;
        add_header Access-Control-Allow-Origin "*" always;
        add_header Cache-Control "public, max-age=300" always;
    }

    location ^~ /t/p/ {
        proxy_pass https://image.tmdb.org/t/p/;
        proxy_ssl_server_name on;
        proxy_set_header Host image.tmdb.org;
        proxy_set_header User-Agent DongguaTV-TMDB-Proxy/1.0;
        add_header Access-Control-Allow-Origin "*" always;
        expires 7d;
    }
}

启用配置并申请证书。Certbot 成功后会自动补上 443 和证书路径:

sudo ln -s /etc/nginx/sites-available/tmdb-proxy /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d tmdb.example.com

浏览器打开 https://tmdb.example.com/ 应看到 {"ok":true}。 回到冬瓜TV 后台,把 https://tmdb.example.com 填进「TMDb 反代根地址」。 不要把这里的域名换成主站域名,也不要填 :3000。

方式 C:宝塔面板反代 TMDb

  1. 在宝塔「网站」里添加站点 tmdb.example.com,PHP 选纯静态,不建数据库。
  2. 进入站点设置 →「SSL」,申请 Let's Encrypt 证书并开启强制 HTTPS。
  3. 进入「反向代理」→「添加反向代理」。第一项:目标 URL 填 https://api.themoviedb.org, 发送域名填 api.themoviedb.org,代理目录填 /3/。
  4. 再添加一项:目标 URL 填 https://image.tmdb.org,发送域名填 image.tmdb.org, 代理目录填 /t/p/。
  5. 如果宝塔版本不支持同一站点配置多条路径代理,直接打开站点「配置文件」,把上面的 Nginx location 段粘进去,再执行 nginx -t 并重载。
  6. 后台「TMDb 反代根地址」填 https://tmdb.example.com,点「测试连接」。

宝塔、Nginx、Cloudflare 三种方式只选一种。TMDb 反代域名和主站域名可以分开, 但不要把主站的 http://127.0.0.1:3000 填进 TMDb 设置。

其他代理方式

在 .env 里填写代理:

HTTPS_PROXY=http://host.docker.internal:7890
HTTP_PROXY=http://host.docker.internal:7890

host.docker.internal 指向宿主机,Compose 已经配置好映射。

All-in-One 则直接把同样的变量写成 -e HTTPS_PROXY=...。

这层代理只解决服务器访问 TMDb 的问题;网站本身的域名和 HTTPS 仍按第八节配置。

接口与访问权限

前台页面和 Android 客户端需要调用同一套只读接口。它们只返回公开数据,不会返回后台密码、TMDb 凭据或采集站鉴权信息。

不能把全部 /api 设成只允许服务器本机访问,否则网页搜索、详情、播放、直播和 Android 客户端都会失效。需要收紧的是跨站调用和后台入口。

默认只允许本站同源

服务端已关闭任意来源 CORS。其他网站即使知道接口地址,浏览器也不能跨站读取本站 API。确实有另一个前端需要跨域调用时,再显式填写完整 Origin:

API_ALLOWED_ORIGINS=https://app.example.com

后台接口必须登录

/api/admin/* 需要管理员会话,未登录返回 401;登录失败次数过多会临时锁定。后台的写操作、备份、采集站导入和客户端编译都不会对未登录请求开放。

把后台限制到本机或固定 IP

如果后台只允许服务器本机访问,设置 ADMIN_ALLOWED_IPS=loopback,再通过 SSH 隧道打开后台。也可以填写固定公网 IP,多个地址用逗号分隔。

ADMIN_ALLOWED_IPS=loopback
# 或
ADMIN_ALLOWED_IPS=203.0.113.10,198.51.100.7

部署在 Nginx、Caddy 或宝塔后面时,还要设置 TRUST_PROXY=1,服务端才能识别真实 IP。直接映射端口时不要开启。

CORS 只能阻止其他网页读取接口,不能阻止用 curl 直接请求公开只读接口。公开浏览类接口仍然必须能被访客浏览器调用;如果要挡机器人和高频请求,请在 Cloudflare 或反向代理层增加访问规则。

常见问题

启动后搜索没有任何结果?

先确认后台已经添加了采集站并且处于「启用」状态。新安装默认是空的。

添加时提示「不是 Maccms JSON 接口」?

说明地址不是 JSON 采集接口。检查是不是少了 /api.php/provide/vod,或者返回的是网页/XML。

非 Maccms 的站点怎么接入?

把适配器文件放进 data/adapters/,后台重载后选择「自定义适配器」。仓库的 examples/adapters/ 有完整示例。

搜索很慢?

关掉响应慢的站点,或者减少启用数量。默认单站 6 秒超时,可用 SEARCH_TIMEOUT 调整。

忘记后台密码了?

改 ADMIN_PASSWORD 环境变量后重启容器即可。

网盘搜索显示暂不可用?

检查 pansou 容器是否在运行、服务器能否访问 Telegram、代理是否正确。

TMDb 首页没有内容?

确认后台「系统设置」里填了 API Key 或 Read Access Token,并点过「测试连接」。如果服务器访问不了 TMDb, 在后台填 Cloudflare Worker 反代地址即可,参考TMDb 反代教程。 也可以继续使用 HTTPS_PROXY 或环境变量里的 TMDB_API_BASE / TMDB_IMAGE_BASE。

更新版本后站点数据会丢吗?

Compose 默认把 ./data 挂到 /app/data, 数据在宿主机的 data/db.json,重建容器不会丢。 用 docker run 时请加 -v dongguatv-data:/app/data; 没有挂载的话,删除容器会一起删掉配置。

必须单独安装 PanSou 吗?

不需要。Compose 和 All-in-One 都自带 PanSou。只有你想用自己的 PanSou 实例时,才需要把 PANSOU_URL 改成那个地址。

怎么备份?

后台「备份与恢复」可以直接下载、创建和恢复备份。服务每天自动备份一次,保留最近 7 份, 文件在 data/backups/。恢复前会再自动备份当前数据。

某条线路播放失败怎么办?

播放器不再自动切源,只提示手动切换其他线路。很多源本身不提供 m3u8,或者只返回 mp4/需要额外解析;换一条线路或换一个搜索结果即可。

后台显示站点冷却中,是坏了吗?

不是永久禁用。PING 连续失败 3 次后会冷却 5 分钟,结束后自动重新参与搜索。可以在后台点「PING 巡检」立即重试。

免责声明

本项目仅作为 Node.js 的学习项目开源,不存储、不发布、不参与任何影视内容的制作与传播。 采集站、网盘资源均由第三方提供,本项目只做技术聚合。

请只接入你有权使用的接口,并遵守当地法律法规。请支持正版影视内容。