【GitHub】深度剖析 iptv-org/iptv:全球最大开源 IPTV 频道库的技术架构

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,原因是:

  1. 权限隔离:Bot 只拥有最小必要权限
  2. 绕过分支保护:Bot 可以直接推送到 master 分支
  3. 审计追踪:所有 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:直播流 URL
  • quality:画质(可选,会自动检测)
  • 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/sdkStream 基类,并扩展了:

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

检测原理:

  1. 使用 axios 发送 HEAD/GET 请求到流 URL
  2. 对于 HLS 流(.m3u8),还会解析 m3u8 文件验证切片 URL 是否可访问
  3. 对于需要特殊 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"
  }
]

十一、技术亮点与踩坑点

✨ 技术亮点

  1. Git 作为数据库:利用 Git 的版本历史作为"审计日志",每次播放列表变更都有完整的 Commit 记录,可追溯任何频道的添加/删除历史。

  2. Issue 驱动的贡献模式:极大降低了贡献门槛,非技术用户也能参与。这种设计值得所有开源项目借鉴。

  3. 多维度播放列表生成:一套数据源,自动生成按国家/语言/类别/城市/行政区划等多种维度的播放列表,满足不同用户需求。

  4. Serverless 架构:完全基于 GitHub 免费服务(Actions + Pages + API),零服务器成本维持全球最大的 IPTV 频道库。

  5. CSV 可编辑性database 仓库的 CSV 文件可以直接用 Google Sheets 打开编辑,进一步降低贡献门槛。

⚠️ 踩坑点

  1. 流 URL 失效问题:直播流 URL 是"易腐资源",平均有效期只有几天到几周。项目每天只能处理一次更新,导致播放列表中总有一定比例的失效链接。

  2. Geo-blocking(地理封锁) :很多流只允许特定国家访问,项目通过 label 字段标记 Geo-blocked,但没有自动化检测手段。

  3. Xtream Codes 服务器问题:这类 IPTV 专用服务器的链接极不稳定,项目明确拒绝接受,但自动识别比较困难(需要发送额外 API 请求验证)。

  4. M3U 文件编码问题:项目特意将所有文件从 LF 转换为 CRLF(Windows 换行符),原因是某些老旧的播放器对 LF 支持不好。

  5. 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 最佳实践的开发者来说,这个项目的源码值得深入研究。


参考资料:


写于 2026年6月 | 作者:码流怪侠