内容
<div align="center">
# 查找媒体流工具
**查找任何网页背后的媒体流。**
[](https://github.com/josesepulvedapino/zahori/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/zahori)
[](https://nodejs.org)
[](./LICENSE)
一个真正的浏览器打开页面,关闭覆盖层,点击播放按钮,并监控网络,直到媒体请求出现。你会得到流 URL(HLS、DASH、MP4 或音频)以及重放所需的头部信息。
</div>
```bash
$ npx zahori get https://example.org/live
https://cdn.example.org/hls/live/master.m3u8
```
无需针对特定网站的配置。无需等待提取器。如果浏览器可以播放,zahori 可以找到它。
## 目录
- [功能](#功能)
- [为什么使用 zahori](#为什么使用-zahori)
- [安装](#安装)
- [命令行界面](#命令行界面)
- [库](#库)
- [配置文件:zahori 如何学习网站](#配置文件:zahori-如何学习网站)
- [教 zahori 一个网站](#教-zahori-一个网站)
- [神谕](#神谕)
- [负责任的使用](#负责任的使用)
- [API](#API)
- [文档](#文档)
- [贡献](#贡献)
- [许可证](#许可证)
## 功能
- **适用于任何 URL,开箱即用。** 内置的通用流程处理大多数页面,无需配置。
- **真正的浏览器引擎。** Playwright 在后台驱动一个无头浏览器;自动处理同意横幅、播放按钮和 iframe 播放器,支持多种语言。
- **包含重放头部信息。** 从真实请求中捕获 Referer/Origin/UA/Cookie,因此 ffmpeg 等工具不会得到 403 错误。
- **HLS 感知。** 主播放列表与媒体播放列表、直播与 VOD 检测,以及多语言流的语言选择。
- **可学习。** 棘手的网站变成一个小的 JSON 配置文件,保存后永远确定。
- **代理原生(MCP)。** 您的编码代理可以探测网站,编写配置文件,测试并保存它。
- **自我修复。** `zahori heal` 在网站更改时修复配置文件,使用 BYO-key 模型,在保存之前由客观神谕验证。
- **拒绝 DRM。** 检测并拒绝受保护的内容。
## 为什么使用 zahori
网络的长尾部分通过嵌入式播放器流式传输,这些播放器没有下载器:本地电视和广播电台、直播活动页面、小型视频门户。[yt-dlp](https://github.com/yt-dlp/yt-dlp) 在其策划的提取器方面非常出色。但长尾部分没有提取器,而每个网站的手动编写提取器并不可行。
| | yt-dlp | zahori |
|---|---|---|
| 覆盖范围 | ~1,800 个精选网站 | 任何浏览器可以打开的页面 |
| 方法 | 网站特定提取器代码 | 真实的浏览器 + 网络嗅探 |
| 新网站 | 等待维护者 | 通用工作,或几分钟内学习 |
| 网站更改 | 提取器在修复前损坏 | `zahori heal` 修复配置文件 |
它们是互补的:对于 YouTube,使用 yt-dlp;对于没有提取器的页面,使用 zahori。
## 安装
```bash
npm install -g zahori # CLI
npm install zahori # 库
```
**要求**
| | |
|---|---|
| 节点 | ≥ 22 |
| 浏览器 | 运行 `npx playwright install chromium --only-shell` 一次;否则使用已安装的 Google Chrome |
| ffmpeg | 可选,仅用于 `--validate`、`--download` 和发现神谕 |
> **pnpm 用户:** pnpm 默认阻止依赖项构建脚本,因此 Playwright 不会自动下载浏览器。运行上面的 `playwright install` 命令一次,或使用系统 Chrome 作为备用。
## 命令行界面
```bash
# 解析页面背后的流 URL(URL 输出到 stdout,其他内容输出到 stderr)
zahori get https://tv.example.org/live
# 完整的 JSON 结果:URL、类型、直播/VOD、重放头部信息、所有候选人
zahori get https://tv.example.org/live --json
# 在多语言流中优先使用音频语言
zahori get https://tv.example.eu/live --lang es
# 录制它(重放捕获的头部信息;需要 ffmpeg)
zahori get https://tv.example.org/live --download session.mp4
zahori get https://tv.example.org/live --audio session.m4a
# 检查流是否为真实媒体,而不是缓冲器或地理阻塞板
zahori get https://tv.example.org/live --validate
# 观察浏览器工作
zahori get https://tv.example.org/live --headful
```
JSON 输出如下所示:
```bash
$ zahori get https://example.org/watch --json
{
"url": "https://cdn.example.org/hls/master.m3u8",
"kind": "hls",
"headers": {
"referer": "https://example.org/",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/150.0.0.0 Safari/537.36"
},
"profileId": "generic",
"candidates": [
"https://cdn.example.org/hls/master.m3u8",
"https://cdn.example.org/hls/720p/index.m3u8",
"https://cdn.example.org/hls/1080p/index.m3u8"
]
}
```
> 解析的 URL 通常是签名的,并且生命周期短暂。zahori 每次运行都会重新派生流,因此请立即使用 URL,永远不要存储它。
## 库
```ts
import { resolve } from 'zahori';
const stream = await resolve('https://tv.example.org/live', {
audioLanguage: 'es', // 可选,用于多语言主音轨
});
stream.url; // 媒体 URL
stream.kind; // 'hls' | 'dash' | 'mp4' | 'audio'
stream.live; // true = 直播,false = VOD,未定义 = 未知
stream.headers; // 重放所需的 Referer/Origin/UA/Cookie(许多 CDN 没有它们会返回 403)
```
将 `stream.headers` 传递给获取 URL 的任何内容。对于 ffmpeg:
```ts
import { download } from 'zahori';
await download(stream, 'session.mp4');
```
错误是类型化的:`NoStreamError`(未捕获任何内容)和 `DrmError`(受保护的内容,被拒绝)。
### 自带浏览器
对于登录网站,在您控制的 Playwright 上下文中驱动 zahori(例如,具有存储状态):
```ts
import { runProfileOnPage, GENERIC_PROFILE } from 'zahori';
const ctx = await browser.newContext({ storageState: 'auth.json' });
const page = await ctx.newPage();
const stream = await runProfileOnPage(page, GENERIC_PROFILE, url);
```
## 配置文件:zahori 如何学习网站
zahori **不附带任何网站知识**。大多数页面都使用内置的通用流程解析。当一个页面无法解析时,解决方法是一个 **配置文件**:一个小的 JSON 文件,描述如何打开该网站(要点击哪些元素,如何识别媒体请求,选择哪个候选流)。配置文件是数据,而不是代码;引擎确定性地解释它们,在运行时没有 AI。
```json
{
"id": "tv-example-org",
"match": ["^https?://([^/]*\\.)?tv\\.example\\.org([/:?#]|$)"],
"steps": [
{ "action": "goto" },
{ "action": "dismissOverlays", "selectors": [] },
{ "action": "click", "selector": ".broadcast-start" }
],
"sniff": { "kinds": ["hls"], "urlPattern": "/live/" }
}
```
配置文件存储在项目中的 `.zahori/profiles/` 目录下(提交它们,以便它们可以前往生产环境和团队成员)或全局 `~/.zahori/profiles/` 目录下。配置文件存储了 **如何重新派生** 流的 URL,而不是流 URL本身,因此签名的或过期的 URL 不会成为问题。
## 教 zahori 一个网站
### 使用您的编码代理(MCP)
zahori 是一个 MCP 服务器:任何具有 MCP 能力的代理都可以探测网站,提出配置文件,测试它与神谕的兼容性,并在通过后保存它,使用您已经拥有的订阅,无需 API 密钥。服务器在协议握手期间将工作流指令发送到代理,因此这在每个客户端中都有效。
大多数客户端使用标准配置:
```json
{
"mcpServers": {
"zahori": { "command": "npx", "args": ["-y", "zahori", "mcp"] }
}
}
```
| 客户端 | 设置 |
|---|---|
| Cursor | 将上面的块添加到 `~/.cursor/mcp.json`(或项目特定的 `.cursor/mcp.json`) |
| VS Code / Copilot | `.vscode/mcp.json`,在 `"servers"` 键下而不是 `"mcpServers"` |
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |
| Claude Code | `claude mcp add zahori -- npx -y zahori mcp` |
| Codex CLI | `codex mcp add zahori -- npx -y zahori mcp` |
| Gemini CLI | `gemini mcp add zahori npx -y zahori mcp` |
| Claude Desktop | 将上面的块添加到 `claude_desktop_config.json` |
然后询问:**“找出 https://tricky.example.org 上的流并保存配置文件”**。公开的工具:`zahori_get`、`zahori_probe`、`zahori_test_profile`、`zahori_save_profile`、`zahori_validate`、`zahori_list_profiles`。
### 无头,使用您自己的密钥
对于无人值守的管道(cron,CI),zahori 可以自己驱动一个模型。它总是尝试免费的确定性路径,只有在失败时才会花费代币:
```bash
export ANTHROPIC_API_KEY=... # 或 OPENAI_API_KEY,或本地 Ollama
zahori discover https://tricky.example.org --model anthropic:claude-sonnet-5
zahori heal https://tricky.example.org # 当保存的配置文件停止工作时
```
模型规格:`anthropic:<model>`、`openai:<model>`、`ollama:<model>`、`compat:<model>`(任何 OpenAI 兼容的端点),或设置 `ZAHORI_MODEL` 一次。模型仅 **提出** 配置文件;引擎运行每个提案,神谕对其进行评分,因此永远不会保存未经验证的内容。
## 神谕
**“是否下载了任何内容”** 是一个弱测试:它会批准广告缓冲器、保持音乐和地理阻塞板。神谕在信任流之前会结合更强的信号。
- **解码探针。** ffmpeg 解码一个有限窗口并测量音量。
- **清单合理性。** 真实的会话有许多分段;板材有四个。
- **沉默容忍度。** 无声窗口触发进一步的探测(沉默可以是合法的休息),仅沉默本身永远不会使流失败。
- **响亮故障。** 403/401 响应被报告为头部问题,而不是谜团。
裁决结果为 `pass`、`fail` 或 `inconclusive`;发现仅在神谕通过的配置文件上保存。
## 负责任的使用
zahori 适用于 **公开可访问的未保护流**:已经为任何访问者在一个普通浏览器中播放的页面。它解析浏览器已经播放的内容;它不破坏任何内容。
**设计上,zahori 拒绝 DRM。** Widevine、PlayReady 和 FairPlay 标记在清单中被检测并拒绝,带有明确的 `DrmError`。它永远不会尝试绕过加密或访问控制;这是一条硬线,而不是设置。
**安全范围。** 最清晰的立场是开放的 HLS/DASH,网站为每个访问者提供,用于合法目的,例如存档、辅助功能、转录和研究。 在使用前,请注意以下几点:
- **“公开”不是“公共领域”。** 流媒体可自由观看并不使其可自由重用;一些网站在其音频和视频输出上附带单独的许可条款。检查网站的重用条款。
- **DRM-free 不是法律上的无保护。** 在某些司法管辖区,绕过甚至是轻量级令牌或签名方案可能会引发反规避问题(美国 DMCA §1201)。zahori 通过拒绝 DRM 并永远不会破坏访问门户来保持安全。
- **在欧盟,** 文本和数据挖掘例外(DSM 指令第 3 和 4 条)允许复制合法可访问的作品,除非版权人以机器可读的方式选择退出。请尊重退出。
您有责任遵守您使用它的网站的服务条款、版权和当地法律。当有疑问时,请优先选择明确开放的内容,并询问机构。
## API
| 导出 | 它做什么 |
|---|---|
| `resolve(url, opts?)` | 解析页面背后的流(配置文件或通用流程) |
| `validate(stream, opts?)` | 神谕:这是真正的媒体吗? |
| `download(stream, path, opts?)` | 使用 ffmpeg 记录,重放头部信息 |
| `probe(url, opts?)` | 用于编写配置文件的结构化页面普查 |
| `testProfile(profile, url, opts?)` | 运行候选配置文件并评分 |
| `saveProfile(profile, dir)` / `loadProfiles()` | 持久化 / 加载配置文件 |
| `discoverProfile(url, opts?)` / `healProfile(profile, url, opts?)` | 模型辅助学习 / 修复 |
| `runProfile(profile, url, opts?)` / `runProfileOnPage(page, ...)` | 执行配置文件(自己的浏览器 / 您的浏览器) |
| `hostPattern(url)` / `profileIdFromUrl(url)` | 配置文件编写助手 |
完整的 TypeScript 类型随软件包一起提供。
## 文档
完整的指南位于 [`docs/`](./docs) 中:
- [CLI](./docs/cli.md):每个命令和标志
- [库 API](./docs/api.md):导出的函数和类型
- [配置文件](./docs/profiles.md):用于教 zahori 一个网站的 JSON 格式
- [MCP](./docs/mcp.md):从编码代理使用 zahori
- [食谱](./docs/recipes.md):端到端示例
## 贡献
欢迎提出问题和 PR。参见 [CONTRIBUTING.md](./CONTRIBUTING.md) 了解开发设置、测试套件和发布过程。
## 许可证
[MIT](./LICENSE) © Jose Sepulveda