视频加载失败

零成本 VIP 视频聚合站:Cloudflare Pages Functions 多源调度架构

零成本 VIP 视频聚合站:Cloudflare Pages Functions 多源调度架构
1573 字
8 分钟
零成本 VIP 视频聚合站:Cloudflare Pages Functions 多源调度架构

这是什么#

一个跑在 Cloudflare Pages 免费额度上的 VIP 视频聚合搜索。前端纯静态 HTML,后端是 Cloudflare Pages Functions(Workers 运行时)写的一组 API,并行请求 5 个 MacCMS 协议的第三方资源站,把同名影片的多个播放源合并,搜一次拿到所有可用线路。

没有服务器,没有数据库,也不用运维。Cloudflare Pages 的 Functions 每天送 10 万次调用,个人站用不完。

整体架构#

上游 MacCMS 资源站

Cloudflare Pages

_shared

Functions

客户端

GET /api/search?wd=xxx

GET /api/detail?ids=xxx

浏览器

API 路由

videoService.mjs

缓存层 caches.default

熔断器 breakerState

数据源配置 sources.mjs

静态资源 index.html + JS

最大资源 zuidapi

量子资源 lziapi

U酷资源 ukuapi

暴风资源 bfzyapi

金鹰资源 jinyingzy

上游 MacCMS 资源站

Cloudflare Pages

_shared

Functions

客户端

GET /api/search?wd=xxx

GET /api/detail?ids=xxx

浏览器

API 路由

videoService.mjs

缓存层 caches.default

熔断器 breakerState

数据源配置 sources.mjs

静态资源 index.html + JS

最大资源 zuidapi

量子资源 lziapi

U酷资源 ukuapi

暴风资源 bfzyapi

金鹰资源 jinyingzy

五个设计#

1. 熔断器:别让坏源拖死整个请求#

5 个上游随时可能挂。每个都傻等到超时(10s),最坏一个搜索请求要等 50 秒。

规则很简单:某个源连续失败 3 次就标记熔断,之后 30 秒跳过它。窗口过了自动半开,放一次试探请求,成功就恢复,失败继续熔断。

// 熔断器状态机
const breakerState = new Map(); // sourceId → { failures, tripped, tripUntil }
function isTripped(sourceId) {
const state = breakerState.get(sourceId);
if (!state) return false;
// 还在熔断窗口内?跳过
if (state.tripped && Date.now() < state.tripUntil) return true;
// 熔断窗口过了,自动重置,允许试探
if (state.tripped && Date.now() >= state.tripUntil) {
state.tripped = false;
state.failures = 0;
}
return false;
}

5 个源里挂掉 4 个,搜索仍然只花 1 个源的时间(1 秒内),不会被超时拖住。

2. 并行聚合:Promise.all 一次打完所有源#

搜索不是挨个轮询,是一次并发出去,在内存里归并:

export async function searchVideos(keyword, page = 1) {
// 并行请求 5 个源
const results = await Promise.all(
sources.map((s) => fetchFromSource(s, { ac: 'videolist', pg: page, wd: keyword }))
);
// 按 vod_name 归并,同一片子的多个源合并
const grouped = new Map();
for (const data of results) {
for (const item of data.list) {
const key = item.vod_name;
if (!grouped.has(key)) {
grouped.set(key, { ...item, _playSources: [] });
}
// 把播放线路合到一起
grouped.get(key)._playSources.push(...parsePlaySources(item));
}
}
return [...grouped.values()];
}

能这么归并是因为 MacCMS 协议的返回字段是统一的,所有资源站都给 vod_name、vod_play_from、vod_play_url 这几个同结构字段,不需要给每个源单独写适配。

3. Cache API:跨请求的边缘缓存#

Worker 里没有 Redis,也写不了本地文件。Cloudflare 给了 caches.default,一个跨请求、跨边缘节点共享的 HTTP 语义缓存。

async function getCache(key) {
const cache = await caches.default;
const res = await cache.match(new Request('https://cache.local/' + key));
if (!res) return null;
const data = await res.json();
// 手动 TTL,Cloudflare Cache API 不会主动过期
if (Date.now() > data._expires) return null;
return data;
}
async function setCache(key, data, ttl = 300) {
const cache = await caches.default;
await cache.put(
new Request('https://cache.local/' + key),
new Response(JSON.stringify({ ...data, _expires: Date.now() + ttl * 1000 }), {
headers: { 'Cache-Control': `max-age=${ttl}` },
})
);
}
为什么用假的 https://cache.local/ URL

Cloudflare Cache API 的 key 必须是完整 URL。缓存的是内部数据,不是真实网页响应,所以虚构一个 URL 当 key 容器,不碰真实流量。

热门词(比如”蜘蛛侠”)第一次查完,之后 5 分钟内所有用户命中的都是同一份缓存,响应从几百毫秒掉到接近零。

4. 后端预判 URL 类型#

上游返回的播放 URL 有好几种:官方平台页面、m3u8 直链、资源站自带播放器的分享页。前端得知道每种该怎么处理:

export function classifyPlayUrl(url) {
// 官方平台 URL → 套第三方解析 API
if (/v\.qq\.com|iqiyi\.com|youku\.com/.test(url)) return 'official';
// m3u8 直链 → hls.js 直接播
if (/\.m3u8(\?|$)/i.test(url)) return 'm3u8';
// 资源站分享页(如 U酷云播、金鹰云播)
// → 页面自带播放器,直接 iframe 嵌入
if (/\/share\/[a-zA-Z0-9]{4,}/i.test(url)) {
if (/ukzy|ukubf/i.test(host)) return 'shareDirect';
return 'share'; // 其他 share 可能需要二次解析
}
return 'unknown';
}

分类放在后端做。前端拿到的 _playSources 数组里,每条线路已经带好 urlType 标签,直接路由到对应播放器组件,前端不用再写一遍正则。

5. 无数据库唯一 ID:SHA-1 就行#

5 个上游的 vod_id 会冲突,不同资源站可能拿同一个数字 ID 表示完全不同的片子。所以要一个全局唯一标识。

不建数据库映射表,直接 SHA-1(影片名 + 年份)取前 20 位十六进制:

export async function generateUniqueId(vodName, vodYear = '') {
const input = `${vodName || ''}|${vodYear || ''}`;
const buf = await crypto.subtle.digest('SHA-1', new TextEncoder().encode(input));
const hex = [...new Uint8Array(buf)]
.slice(0, 10)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
return 'av_' + hex;
}

同一部片永远得到同一个 ID,不同片几乎不可能碰撞(2^80 的空间)。纯函数,不需要数据库、缓存表或 ID 分配服务。

为什么选 Cloudflare Pages Functions#

维度Cloudflare PagesNode.js + VPS
服务器成本0(每天 10 万次免费)至少 20 元/月
运维0(Git push 自动部署)进程守护、PM2、日志
延迟<50ms(全球边缘节点直出)取决于 VPS 位置
缓存内置 Cache API(跨边缘共享)自己搭 Redis
运行时Web Standard API(fetch/AbortSignal)Node 原生模块
限制CPU 50ms / 请求(免费版)无

50ms CPU 限制对这个项目够用。计算全是 Promise.all 网络 IO、JSON 解析和数组归并,真正占 CPU 的逻辑是微秒级。瓶颈在上游 API 的响应时间,不在 Worker。

局限和取舍#

几个绕不开的短板:

  1. 上游 API 全挂就是全挂,熔断器只能降级,变不出数据
  2. CPU 50ms 限制:哪天聚合到 100 个源,JSON 解析可能超
  3. 没有持久化:Worker 重启后内存里的熔断器状态会丢,好在 caches.default 是持久的,重试几次自己就重建了
  4. MacCMS 协议耦合:加非 MacCMS 的源要写适配层

对照表#

设计解决什么问题关键代码
熔断器坏源拖慢请求breakerState Map + 3 次失败熔断 30s
Promise.all 并行聚合延迟最小化sources.map → fetch → merge
Cache API跨请求/跨节点缓存caches.default + 手动 _expires TTL
URL 分类前端播放路由后端正则预判,返回 urlType 标签
SHA-1 唯一 ID无数据库去重crypto.subtle.digest 纯函数

后端不到 450 行,跑在 Cloudflare 免费额度上,撑得住每天几千次搜索。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

零成本 VIP 视频聚合站:Cloudflare Pages Functions 多源调度架构
https://www.cpdd520.top/posts/vip-video-parser-architecture/
作者
小鼠宝
发布于
2026-08-31
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
小鼠宝
来和我一起打洲吧!
分类
标签
最新动态
站点统计
文章
12
分类
4
标签
41
总字数
13,045
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
EdgeOne Pages
博客版本
Firefly v6.16.7
文章许可
CC BY-NC-SA 4.0
文章目录