GitHub: https://github.com/iptv-org/iptv
Stars: 90k+ | Forks: 6.8k+ | Commits: 34,000+
作者:freearhey | 许可证:MIT
一、项目简介
iptv-org/iptv 是全球最大的开源 IPTV 直播源聚合项目。它不存储任何视频文件,而是通过社区贡献的方式,收集全球各地电视台的公开直播流 URL,整理成标准的 M3U 播放列表,供任何人免费使用。
核心数据(2026年6月):
- 覆盖 150+ 个国家/地区
- 支持 150+ 种语言
- 按类别分组 31 种(新闻/体育/电影/音乐等)
- 主播放列表:
https://iptv-org.github.io/iptv/index.m3u
二、整体架构:多仓库微服务生态
这个项目最有趣的地方在于------它是一个由多个 GitHub 仓库协同运作的"分布式系统",而非单一代码库。
┌─────────────────────────────────────────────────────────┐
│ iptv-org 生态 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ iptv │ │ database │ │ api │ │
│ │ (播放列表) │◄───│(频道元数据)│───►│ (JSON API)│ │
│ └────┬─────┘ └──────────┘ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ GitHub │ ┌──────────┐ ┌──────────┐ │
│ │ Pages │ │ epg │ │awesome- │ │
│ │(静态托管) │ │(节目单) │ │ iptv │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
各仓库职责
| 仓库 | 职责 | 核心技术 |
|---|---|---|
| iptv | 存储 M3U 播放列表文件(streams/ 目录) | TypeScript, M3U |
| database | 存储频道元数据(CSV 格式) | CSV, Google Sheets 可编辑 |
| api | 提供 REST JSON API(自动从 database 生成) | GitHub Pages 静态托管 |
| epg | 提供 EPG(电子节目单)数据 | XMLTV 格式 |
| awesome-iptv | 精选 IPTV 相关资源链接集合 | Markdown |
关键设计思想: 数据与播放列表分离。频道名称、Logo、分类等"慢变化"数据放在 database,而随时可能失效的直播流 URL 放在 iptv。两者通过 channel_id 关联。
三、核心技术栈
json
{
"runtime": "Node.js 22",
"language": "TypeScript",
"test": "Jest + @swc/jest",
"lint": "ESLint + typescript-eslint",
"key_deps": {
"iptv-playlist-parser": "M3U 文件解析",
"hls-parser": "HLS 流解析",
"mediainfo.js": "获取流媒体信息(分辨率等)",
"m3u-linter": "M3U 语法检查",
"@iptv-org/sdk": "共享数据模型(database/api 共用)",
"@freearhey/core": "自定义 Collection/Logger 工具库",
"@octokit/core": "GitHub API 调用(处理 Issue/PR)"
}
}
有趣的点: 项目没有用任何传统数据库(MySQL/PostgreSQL/MongoDB),而是用 CSV 文件 + Git 作为数据库,用 GitHub Issues 作为"工单系统",用 GitHub Actions 作为"后台任务调度器"。这是一个完全基于 Git 的 Serverless 架构。
四、数据模型设计
4.1 CSV "数据库" 设计
iptv-org/database 仓库的 /data 目录下有 11 个 CSV 文件:
data/
├── channels.csv # 频道基础信息(核心表)
├── feeds.csv # 频道的区域馈送(如 France3 有巴黎版/北加莱版)
├── logos.csv # 频道 Logo 图片 URL
├── categories.csv # 分类(新闻/体育/电影...)
├── languages.csv # 语言(ISO 639-3 标准)
├── countries.csv # 国家(ISO 3166-1 alpha-2 标准)
├── subdivisions.csv # 行政区划(省/州,ISO 3166-2)
├── cities.csv # 城市(UN/LOCODE 标准)
├── regions.csv # 地理区域(如"马格里布")
├── timezones.csv # 时区(tz database)
└── blocklist.csv # 被屏蔽的频道(DMCA/NSFW)
channels.csv 字段设计:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string | 唯一 ID,如 CCTV1.cn,格式:<名称>.<国家代码> |
name |
string | 频道全称 |
alt_names |
JSON数组 | 别名(如 ["安徽卫视"]) |
network |
string | 所属电视网 |
country |
string | 来源国家(ISO 3166-1 alpha-2) |
categories |
JSON数组 | 分类 ID 列表 |
is_nsfw |
boolean | 是否成人内容 |
launched |
date | 开播日期 |
closed |
date | 停播日期 |
website |
string | 官方网站 |
4.2 播放列表与数据库的关联
#mermaid-svg-JKOS98UanxCdDOTa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JKOS98UanxCdDOTa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JKOS98UanxCdDOTa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JKOS98UanxCdDOTa .error-icon{fill:#552222;}#mermaid-svg-JKOS98UanxCdDOTa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JKOS98UanxCdDOTa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JKOS98UanxCdDOTa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JKOS98UanxCdDOTa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JKOS98UanxCdDOTa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JKOS98UanxCdDOTa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JKOS98UanxCdDOTa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JKOS98UanxCdDOTa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JKOS98UanxCdDOTa .marker.cross{stroke:#333333;}#mermaid-svg-JKOS98UanxCdDOTa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JKOS98UanxCdDOTa p{margin:0;}#mermaid-svg-JKOS98UanxCdDOTa .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JKOS98UanxCdDOTa .cluster-label text{fill:#333;}#mermaid-svg-JKOS98UanxCdDOTa .cluster-label span{color:#333;}#mermaid-svg-JKOS98UanxCdDOTa .cluster-label span p{background-color:transparent;}#mermaid-svg-JKOS98UanxCdDOTa .label text,#mermaid-svg-JKOS98UanxCdDOTa span{fill:#333;color:#333;}#mermaid-svg-JKOS98UanxCdDOTa .node rect,#mermaid-svg-JKOS98UanxCdDOTa .node circle,#mermaid-svg-JKOS98UanxCdDOTa .node ellipse,#mermaid-svg-JKOS98UanxCdDOTa .node polygon,#mermaid-svg-JKOS98UanxCdDOTa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JKOS98UanxCdDOTa .rough-node .label text,#mermaid-svg-JKOS98UanxCdDOTa .node .label text,#mermaid-svg-JKOS98UanxCdDOTa .image-shape .label,#mermaid-svg-JKOS98UanxCdDOTa .icon-shape .label{text-anchor:middle;}#mermaid-svg-JKOS98UanxCdDOTa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JKOS98UanxCdDOTa .rough-node .label,#mermaid-svg-JKOS98UanxCdDOTa .node .label,#mermaid-svg-JKOS98UanxCdDOTa .image-shape .label,#mermaid-svg-JKOS98UanxCdDOTa .icon-shape .label{text-align:center;}#mermaid-svg-JKOS98UanxCdDOTa .node.clickable{cursor:pointer;}#mermaid-svg-JKOS98UanxCdDOTa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JKOS98UanxCdDOTa .arrowheadPath{fill:#333333;}#mermaid-svg-JKOS98UanxCdDOTa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JKOS98UanxCdDOTa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JKOS98UanxCdDOTa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JKOS98UanxCdDOTa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JKOS98UanxCdDOTa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JKOS98UanxCdDOTa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JKOS98UanxCdDOTa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JKOS98UanxCdDOTa .cluster text{fill:#333;}#mermaid-svg-JKOS98UanxCdDOTa .cluster span{color:#333;}#mermaid-svg-JKOS98UanxCdDOTa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JKOS98UanxCdDOTa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JKOS98UanxCdDOTa rect.text{fill:none;stroke-width:0;}#mermaid-svg-JKOS98UanxCdDOTa .icon-shape,#mermaid-svg-JKOS98UanxCdDOTa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JKOS98UanxCdDOTa .icon-shape p,#mermaid-svg-JKOS98UanxCdDOTa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JKOS98UanxCdDOTa .icon-shape .label rect,#mermaid-svg-JKOS98UanxCdDOTa .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JKOS98UanxCdDOTa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JKOS98UanxCdDOTa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JKOS98UanxCdDOTa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} tvg-id
stream URL
可能失效
失效
M3U 播放列表
channels.csv
feeds.csv
logos.csv
实时流地址
定期检测脚本
自动移除
核心关联键是 tvg-id,格式为 <channel_id>@<feed_id>(feed_id 可选)。
五、M3U 播放列表格式深度解析
5.1 标准 M3U vs iptv 扩展格式
标准 M3U(#EXTINF 基础):
#EXTM3U
#EXTINF:-1,Channel Name
http://example.com/stream.m3u8
iptv 项目使用的扩展格式:
m3u
#EXTINF:-1 tvg-id="CCTV1.cn" tvg-logo="https://example.com/logo.png" group-title="News",CCTV-1 (1080p)
https://example.com/stream.m3u8
扩展属性说明:
| 属性 | 说明 | 示例 |
|---|---|---|
tvg-id |
关联到 database 中的频道 ID | CCTV1.cn |
tvg-logo |
频道 Logo URL | https://... |
group-title |
播放器中的分组名 | News |
http-referrer |
自定义 HTTP Referer(防盗链) | http://example.com/ |
http-user-agent |
自定义 User-Agent | Mozilla/5.0 ... |
5.2 #EXTVLCOPT 指令
对于需要特殊 HTTP 头才能访问的流,使用 #EXTVLCOPT 指令:
m3u
#EXTINF:-1 tvg-id="ExampleTV.us",Example TV
#EXTVLCOPT:http-referrer=http://example.com/
#EXTVLCOPT:http-user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64)
http://example.com/stream.m3u8
这是 VLC 播放器定义的扩展指令,大部分 IPTV 播放器都支持。
六、自动化工作流:GitHub Actions 驱动的"无人值守"维护
这是整个项目最精彩的技术设计------完全依靠 GitHub Actions 实现每日自动化维护,几乎不需要人工干预。
6.1 三个核心 Workflow
┌──────────────────────────────────────────────────────┐
│ GitHub Actions │
├──────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ │
│ │ check.yml │ 触发:PR 提交 │
│ │ (PR检查) │ → 运行 lint + validate │
│ └───────────────┘ │
│ │ │
│ ▼ 通过 │
│ ┌───────────────┐ │
│ │ format.yml │ 手动触发 │
│ │ (格式化) │ → 格式化 M3U 文件 │
│ └───────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ update.yml │ 每天 UTC 0:00 自动触发 │
│ │ (每日更新) │ → 处理 Issue 请求 │
│ │ │ → 生成公开播放列表 │
│ │ │ → 部署到 GitHub Pages │
│ │ │ → 更新 API JSON │
│ └───────────────┘ │
│ │
└──────────────────────────────────────────────────────┘
6.2 update.yml 完整流程拆解
yaml
# 每天 UTC 0:00 自动运行(即北京时间早上 8:00)
schedule:
- cron: '0 0 * * *'
执行步骤:
iptv-org/api GitHub Pages iptv 仓库 GitHub Actions iptv-bot iptv-org/api GitHub Pages iptv 仓库 GitHub Actions iptv-bot #mermaid-svg-cEFsWr86QsQewfJ2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cEFsWr86QsQewfJ2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cEFsWr86QsQewfJ2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cEFsWr86QsQewfJ2 .error-icon{fill:#552222;}#mermaid-svg-cEFsWr86QsQewfJ2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cEFsWr86QsQewfJ2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cEFsWr86QsQewfJ2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cEFsWr86QsQewfJ2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cEFsWr86QsQewfJ2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cEFsWr86QsQewfJ2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cEFsWr86QsQewfJ2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cEFsWr86QsQewfJ2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cEFsWr86QsQewfJ2 .marker.cross{stroke:#333333;}#mermaid-svg-cEFsWr86QsQewfJ2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cEFsWr86QsQewfJ2 p{margin:0;}#mermaid-svg-cEFsWr86QsQewfJ2 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-cEFsWr86QsQewfJ2 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-cEFsWr86QsQewfJ2 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-cEFsWr86QsQewfJ2 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-cEFsWr86QsQewfJ2 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-cEFsWr86QsQewfJ2 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-cEFsWr86QsQewfJ2 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-cEFsWr86QsQewfJ2 .sequenceNumber{fill:white;}#mermaid-svg-cEFsWr86QsQewfJ2 #sequencenumber{fill:#333;}#mermaid-svg-cEFsWr86QsQewfJ2 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-cEFsWr86QsQewfJ2 .messageText{fill:#333;stroke:none;}#mermaid-svg-cEFsWr86QsQewfJ2 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-cEFsWr86QsQewfJ2 .labelText,#mermaid-svg-cEFsWr86QsQewfJ2 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-cEFsWr86QsQewfJ2 .loopText,#mermaid-svg-cEFsWr86QsQewfJ2 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-cEFsWr86QsQewfJ2 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-cEFsWr86QsQewfJ2 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-cEFsWr86QsQewfJ2 .noteText,#mermaid-svg-cEFsWr86QsQewfJ2 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-cEFsWr86QsQewfJ2 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-cEFsWr86QsQewfJ2 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-cEFsWr86QsQewfJ2 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-cEFsWr86QsQewfJ2 .actorPopupMenu{position:absolute;}#mermaid-svg-cEFsWr86QsQewfJ2 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-cEFsWr86QsQewfJ2 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-cEFsWr86QsQewfJ2 .actor-man circle,#mermaid-svg-cEFsWr86QsQewfJ2 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-cEFsWr86QsQewfJ2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 处理已批准的 Issue(添加/编辑/删除流) 生成所有公开播放列表(categories/languages/countries/...) 生成 .api/streams.json 1. Checkout 代码2. npm install3. npm run playlist:update4. npm run playlist:lint5. npm run playlist:validate6. npm run playlist:generate7. npm run playlist:export8. 部署播放列表到 gh-pages 分支9. 部署 streams.json 到 api 仓库10. Commit & Push 所有变更
6.3 iptv-bot 的 GitHub App 身份
项目使用 GitHub App(iptv-bot) 而不是 Personal Access Token,原因是:
- 权限隔离:Bot 只拥有最小必要权限
- 绕过分支保护:Bot 可以直接推送到 master 分支
- 审计追踪:所有 Bot 操作都有明确的身份标识
七、贡献机制:Issue 驱动的"无 PR"贡献模式
这是该项目社区运营 上的创新------普通用户不需要懂 Git,只需要提交 GitHub Issue 就可以贡献频道!
7.1 Issue 模板机制
项目配置了三个 Issue 模板:
.github/ISSUE_TEMPLATE/
├── 1_streams_add.yml # 请求添加新流
├── 2_streams_edit.yml # 请求编辑现有流
└── 3_streams_report.yml # 报告失效流
添加流的 Issue 表单字段:
stream_id:频道 ID(必须从 iptv-org.github.io 查询得到)stream_url:直播流 URLquality:画质(可选,会自动检测)label:标签(如Geo-blocked/Not 24/7)http_user_agent/http_referrer:防盗链参数
7.2 自动化处理流程
用户提交 Issue
│
▼
维护者审核(打 approved 标签)
│
▼
次日 UTC 0:00 GitHub Actions 自动运行
│
├── 读取 approved Issue
├── 解析 Issue 表单数据
├── 更新 streams/*.m3u 文件
└── Commit 并关闭 Issue
关键代码逻辑(scripts/commands/playlist/update.ts):
typescript
async function processIssues(issues: Collection<Issue>) {
const requests = issues.filter(issue => issue.labels.includes('approved'))
for (const issue of requests) {
if (issue.labels.includes('streams:remove')) {
await removeStream(issue)
} else if (issue.labels.includes('streams:edit')) {
await editStream(issue)
} else if (issue.labels.includes('streams:add')) {
await addStream(issue)
}
}
}
八、源码深度拆解
8.1 Stream 模型(核心数据模型)
srcipts/models/stream.ts 是整个项目的核心,它继承了 @iptv-org/sdk 的 Stream 基类,并扩展了:
typescript
export class Stream extends sdk.Models.Stream {
filepath?: string // 该流所属的 M3U 文件路径(如 cn.m3u)
line?: number // 在文件中的行号
groupTitle: string // 播放器中的分组名
removed: boolean // 标记是否已删除(软删除)
tvgId?: string // 从 #EXTINF 解析出的 tvg-id
statusCode?: string // 流检测状态码
guides // 关联的 EPG 节目单
}
从 M3U 解析 Stream 对象的过程:
typescript
static fromPlaylistItem(data: parser.PlaylistItem): Stream {
// 解析 tvg-id="CCTV1.cn@East" → channelId + feedId
const [channelId, feedId] = data.tvg.id.split('@')
// 解析标题中的 quality 和 label
// "CCTV-1 (1080p) [Geo-blocked]" → title/CCTV-1, quality/1080p, label/Geo-blocked
const { title, label, quality } = parseName(data.name)
return new Stream({
channel: channelId,
feed: feedId,
title,
quality,
url: data.url,
referrer: data.http.referrer,
user_agent: data.http['user-agent'],
label
})
}
8.2 PlaylistParser(M3U 解析器)
typescript
export class PlaylistParser {
storage: Storage
async parse(files: string[]): Promise<Collection<Stream>> {
const parsed = new Collection<Stream>()
for (const filepath of files) {
const _parsed = await this.parseFile(filepath)
_parsed.forEach(item => parsed.add(item))
}
return parsed // 返回所有文件的 Stream 集合
}
async parseFile(filepath: string): Promise<Collection<Stream>> {
const content = await this.storage.load(filepath)
const parsed = parser.parse(content) // 使用 iptv-playlist-parser 库
const streams = new Collection<Stream>()
parsed.items.forEach(data => {
const stream = Stream.fromPlaylistItem(data)
stream.filepath = filepath
streams.add(stream)
})
return streams
}
}
8.3 Generator 系统(播放列表生成器)
scripts/generators/ 目录下有多个 Generator 类,负责从"内部播放列表"(按国家组织)生成"公开播放列表"(按类别/语言/国家等维度组织)。
生成流程:
typescript
// scripts/commands/playlist/generate.ts 核心逻辑
async function main() {
// 1. 加载所有内部播放列表
let streams = await parser.parse(files)
// 2. 排序(先按 ID,再按分辨率降序,最后按 label 降序)
streams = streams.sortBy(
[stream => stream.getId(),
stream => stream.getVerticalResolution(),
stream => stream.label],
['asc', 'desc', 'desc']
)
// 3. 去重(相同 ID 的流只保留第一条,即最高画质的)
streams = streams.uniqBy(stream => stream.getId())
// 4. 生成各维度播放列表
await new CategoriesGenerator({ categories, streams }).generate()
await new LanguagesGenerator({ streams }).generate()
await new CountriesGenerator({ countries, streams }).generate()
await new SubdivisionsGenerator({ subdivisions, streams }).generate()
await new CitiesGenerator({ cities, streams }).generate()
// ...
}
九、流检测与质量控制
9.1 playlist:test 命令
项目内置了流 URL 有效性检测功能:
bash
# 检测单个播放列表
npm run playlist:test streams/cn.m3u
# 自动修复(移除失效流)
npm run playlist:test streams/cn.m3u --- --fix
检测原理:
- 使用
axios发送 HEAD/GET 请求到流 URL - 对于 HLS 流(
.m3u8),还会解析 m3u8 文件验证切片 URL 是否可访问 - 对于需要特殊 User-Agent/Referer 的流,会自动带上相应请求头
检测结果示例:
streams/fr.m3u
┌─────┬─────────────────┬─────────────────────────────┬─────────────┬─────────────────────┐
│ │ tvg-id │ url │ label │ status │
├─────┼─────────────────┼─────────────────────────────┼─────────────┼─────────────────────┤
│ 0 │ 6ter.fr │ https://origin-... │ │ LOADING... │
│ 1 │ 20MinutesTV.fr │ https://lives... │ │ FFMPEG_STREAMS_NOT_ │
│ │ │ │ │ FOUND │
│ 3 │ ADNTVPlus.fr │ https://samsunguk-... │ Geo-blocked │ HTTP_FORBIDDEN │
│ 4 │ Africa24.fr │ https://edge12... │ │ OK │
└─────┴─────────────────┴─────────────────────────────┴─────────────┴─────────────────────┘
9.2 m3u-linter 语法检查
项目使用 m3u-linter 进行 M3U 文件语法检查,配置文件为 m3u-linter.json:
json
{
"rules": {
"tvg-id-format": "error",
"url-reachable": "warn",
"duplicate-entries": "error"
}
}
十、API 设计深度解析
iptv-org/api 仓库通过 GitHub Pages 提供静态 JSON API,所有数据从 iptv-org/database 自动生成。
10.1 核心端点
| 端点 | 说明 | 数据来源 |
|---|---|---|
/api/channels.json |
所有频道基础信息 | database |
/api/streams.json |
所有直播流 URL | iptv |
/api/feeds.json |
频道区域馈送信息 | database |
/api/logos.json |
频道 Logo | database |
/api/guides.json |
EPG 节目单来源 | epg |
/api/categories.json |
分类列表 | database |
/api/languages.json |
语言列表 | database |
/api/countries.json |
国家列表(含国旗 emoji) | database |
/api/blocklist.json |
屏蔽列表 | database |
10.2 streams.json 数据结构
json
[
{
"channel": "France3.fr",
"feed": "NordPasdeCalaisHD",
"title": "France 3 Nord Pas-de-Calais HD",
"url": "http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8",
"referrer": "http://example.com/",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"quality": "720p",
"label": "Geo-blocked"
}
]
十一、技术亮点与踩坑点
✨ 技术亮点
-
Git 作为数据库:利用 Git 的版本历史作为"审计日志",每次播放列表变更都有完整的 Commit 记录,可追溯任何频道的添加/删除历史。
-
Issue 驱动的贡献模式:极大降低了贡献门槛,非技术用户也能参与。这种设计值得所有开源项目借鉴。
-
多维度播放列表生成:一套数据源,自动生成按国家/语言/类别/城市/行政区划等多种维度的播放列表,满足不同用户需求。
-
Serverless 架构:完全基于 GitHub 免费服务(Actions + Pages + API),零服务器成本维持全球最大的 IPTV 频道库。
-
CSV 可编辑性 :
database仓库的 CSV 文件可以直接用 Google Sheets 打开编辑,进一步降低贡献门槛。
⚠️ 踩坑点
-
流 URL 失效问题:直播流 URL 是"易腐资源",平均有效期只有几天到几周。项目每天只能处理一次更新,导致播放列表中总有一定比例的失效链接。
-
Geo-blocking(地理封锁) :很多流只允许特定国家访问,项目通过
label字段标记Geo-blocked,但没有自动化检测手段。 -
Xtream Codes 服务器问题:这类 IPTV 专用服务器的链接极不稳定,项目明确拒绝接受,但自动识别比较困难(需要发送额外 API 请求验证)。
-
M3U 文件编码问题:项目特意将所有文件从 LF 转换为 CRLF(Windows 换行符),原因是某些老旧的播放器对 LF 支持不好。
-
GitHub API 限流:每天处理大量 Issue 时,GitHub API 的 Rate Limit 经常成为瓶颈,项目通过 GitHub App 的 Token 来缓解。
十二、如何使用这些播放列表
方式一:直接 URL 播放
将以下任意 URL 粘贴到支持 M3U 的播放器中:
# 主播放列表(全部频道)
https://iptv-org.github.io/iptv/index.m3u
# 按国家(中国)
https://iptv-org.github.io/iptv/countries/cn.m3u
# 按语言(中文)
https://iptv-org.github.io/iptv/languages/zho.m3u
# 按类别(新闻)
https://iptv-org.github.io/iptv/categories/news.m3u
方式二:下载到本地
bash
# 下载中国频道播放列表
curl -o cn.m3u https://iptv-org.github.io/iptv/countries/cn.m3u
# 用 VLC 播放
vlc cn.m3u
方式三:在代码中调用 API
javascript
// 获取所有中文频道信息
fetch('https://iptv-org.github.io/api/channels.json')
.then(res => res.json())
.then(channels => {
const chineseChannels = channels.filter(ch => ch.country === 'CN')
console.log(chineseChannels)
})
十三、总结
iptv-org/iptv 是一个教科书级别的开源项目,它展示了如何:
- 用 GitHub Actions 构建完全自动化的维护流程
- 用 Issue 模板 降低社区贡献门槛
- 用 GitHub Pages 实现零成本的 API 和静态资源托管
- 用 CSV + Git 替代传统数据库,实现"人人可编辑"的数据管理
- 用 Monorepo 多仓库协作 实现关注点分离
对于想学习自动化运维、开源社区运营、CI/CD 最佳实践的开发者来说,这个项目的源码值得深入研究。
参考资料:
- 项目主页:https://github.com/iptv-org/iptv
- 在线数据库浏览:https://iptv-org.github.io/
- API 文档:https://github.com/iptv-org/api
- EPG 项目:https://github.com/iptv-org/epg
- awesome-iptv 资源集合:https://github.com/iptv-org/awesome-iptv
写于 2026年6月 | 作者:码流怪侠