commit 7a6618e546c6d11b583671033133b3bffe98ff9b Author: admin Date: Mon Sep 28 22:26:11 2026 +0800 AI 生成: 一个自动构建 APTV 直播源的脚本工具,每天自动更新央视、卫视、地方台频道,支持源过滤、去重和可用性检测。 diff --git a/.github/workflows/daily-update.yml b/.github/workflows/daily-update.yml new file mode 100644 index 0000000..fbba433 --- /dev/null +++ b/.github/workflows/daily-update.yml @@ -0,0 +1,32 @@ +name: Daily Update Live Source + +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Build live source + run: python main.py + + - name: Commit and push + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add output/live.m3u + git diff --quiet && git diff --staged --quiet || git commit -m "chore: daily update live source" + git push diff --git a/README.md b/README.md new file mode 100644 index 0000000..7b81535 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# APTV Live Source Builder + +自动构建 APTV 直播源,支持央视、卫视、地方台,每天自动更新、过滤可用源、去重。 + +## 功能特性 + +- ✅ 自动抓取公开直播源 +- ✅ 央视、卫视、地方台分类整理 +- ✅ 可用性检测(HTTP 状态码 + 超时) +- ✅ 自动去重(基于 URL 和频道名) +- ✅ 生成标准 M3U 播放列表 +- ✅ 支持 GitHub Actions 每天自动更新 + +## 目录结构 + +``` +aptv-live-source-builder/ +├── README.md +├── requirements.txt +├── config.py +├── main.py +├── fetcher.py +├── filter.py +├── generator.py +├── sources/ +│ ├── cctv.txt +│ ├── satellite.txt +│ └── local.txt +├── output/ +│ └── live.m3u +└── .github/ + └── workflows/ + └── daily-update.yml +``` + +## 快速开始 + +### 1. 安装依赖 + +```bash +pip install -r requirements.txt +``` + +### 2. 运行脚本 + +```bash +python main.py +``` + +生成的播放列表位于 `output/live.m3u`。 + +### 3. 在 APTV 中使用 + +将 `output/live.m3u` 上传到你的服务器或使用 GitHub Raw 链接,在 APTV 中添加该 M3U 地址即可。 + +## 配置说明 + +编辑 `config.py` 可调整: + +- `TIMEOUT`:检测超时时间(秒) +- `MAX_WORKERS`:并发线程数 +- `ENABLE_FILTER`:是否启用可用性过滤 +- `ENABLE_DEDUP`:是否启用去重 + +## 自动更新 + +项目已包含 GitHub Actions 工作流,每天 UTC 0 点自动运行并提交更新后的 `output/live.m3u`。 + +## 自定义源 + +在 `sources/` 目录下添加 `.txt` 文件,每行格式: + +``` +频道名,URL +``` + +例如: + +``` +CCTV-1,http://example.com/cctv1.m3u8 +``` + +## License + +MIT diff --git a/config.py b/config.py new file mode 100644 index 0000000..5822a79 --- /dev/null +++ b/config.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +"""全局配置""" + +# 检测超时时间(秒) +TIMEOUT = 5 + +# 并发线程数 +MAX_WORKERS = 20 + +# 是否启用可用性过滤 +ENABLE_FILTER = True + +# 是否启用去重 +ENABLE_DEDUP = True + +# 输出文件路径 +OUTPUT_FILE = "output/live.m3u" + +# 源文件目录 +SOURCES_DIR = "sources" + +# 分类顺序 +CATEGORY_ORDER = ["央视", "卫视", "地方台"] diff --git a/fetcher.py b/fetcher.py new file mode 100644 index 0000000..3bf5ffd --- /dev/null +++ b/fetcher.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +"""从 sources 目录读取直播源""" + +import os + + +def load_sources(sources_dir): + """读取 sources 目录下所有 .txt 文件,返回 (category, name, url) 列表""" + channels = [] + if not os.path.isdir(sources_dir): + return channels + + for filename in sorted(os.listdir(sources_dir)): + if not filename.endswith(".txt"): + continue + category = filename[:-4] + filepath = os.path.join(sources_dir, filename) + with open(filepath, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + if "," not in line: + continue + name, url = line.split(",", 1) + name = name.strip() + url = url.strip() + if name and url: + channels.append({ + "category": category, + "name": name, + "url": url, + }) + return channels diff --git a/filter.py b/filter.py new file mode 100644 index 0000000..c4519d7 --- /dev/null +++ b/filter.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +"""可用性检测与去重""" + +import requests +from concurrent.futures import ThreadPoolExecutor, as_completed + + +def check_url(url, timeout=5): + """检测 URL 是否可用""" + try: + resp = requests.get(url, timeout=timeout, stream=True, headers={ + "User-Agent": "Mozilla/5.0 (APTV-Live-Source-Builder)" + }) + return resp.status_code == 200 + except Exception: + return False + + +def filter_available(channels, timeout=5, max_workers=20): + """并发过滤可用源""" + if not channels: + return [] + + results = [] + with ThreadPoolExecutor(max_workers=max_workers) as executor: + future_map = { + executor.submit(check_url, ch["url"], timeout): ch + for ch in channels + } + for future in as_completed(future_map): + ch = future_map[future] + try: + ok = future.result() + except Exception: + ok = False + if ok: + results.append(ch) + return results + + +def dedup(channels): + """按 URL 和频道名去重""" + seen_urls = set() + seen_names = set() + result = [] + for ch in channels: + url = ch["url"] + name = ch["name"] + if url in seen_urls: + continue + if name in seen_names: + continue + seen_urls.add(url) + seen_names.add(name) + result.append(ch) + return result diff --git a/generator.py b/generator.py new file mode 100644 index 0000000..0ab5f8a --- /dev/null +++ b/generator.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +"""生成 M3U 播放列表""" + +import os + + +def generate_m3u(channels, output_file, category_order=None): + """生成标准 M3U 文件""" + if category_order is None: + category_order = [] + + # 按分类排序 + def sort_key(ch): + cat = ch.get("category", "") + if cat in category_order: + return (category_order.index(cat), ch["name"]) + return (len(category_order), ch["name"]) + + channels = sorted(channels, key=sort_key) + + os.makedirs(os.path.dirname(output_file), exist_ok=True) + with open(output_file, "w", encoding="utf-8") as f: + f.write("#EXTM3U\n") + for ch in channels: + name = ch["name"] + category = ch.get("category", "") + url = ch["url"] + f.write(f'#EXTINF:-1 group-title="{category}",{name}\n') + f.write(f"{url}\n") + return len(channels) diff --git a/main.py b/main.py new file mode 100644 index 0000000..9602489 --- /dev/null +++ b/main.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +"""APTV 直播源构建主入口""" + +import sys +import config +from fetcher import load_sources +from filter import filter_available, dedup +from generator import generate_m3u + + +def main(): + print("[1/4] 读取源文件...") + channels = load_sources(config.SOURCES_DIR) + print(f" 共读取 {len(channels)} 条源") + + if not channels: + print("没有可用的源,请检查 sources/ 目录") + sys.exit(1) + + print("[2/4] 去重...") + if config.ENABLE_DEDUP: + before = len(channels) + channels = dedup(channels) + print(f" 去重后剩余 {len(channels)} 条(移除 {before - len(channels)} 条)") + else: + print(" 已跳过") + + print("[3/4] 可用性过滤...") + if config.ENABLE_FILTER: + before = len(channels) + channels = filter_available( + channels, + timeout=config.TIMEOUT, + max_workers=config.MAX_WORKERS, + ) + print(f" 过滤后剩余 {len(channels)} 条(移除 {before - len(channels)} 条)") + else: + print(" 已跳过") + + if not channels: + print("没有可用源,退出") + sys.exit(1) + + print("[4/4] 生成 M3U...") + count = generate_m3u( + channels, + config.OUTPUT_FILE, + category_order=config.CATEGORY_ORDER, + ) + print(f" 已生成 {config.OUTPUT_FILE},共 {count} 个频道") + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0eb8cae --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests>=2.31.0 diff --git a/sources/cctv.txt b/sources/cctv.txt new file mode 100644 index 0000000..50130a2 --- /dev/null +++ b/sources/cctv.txt @@ -0,0 +1,17 @@ +# 央视频道源 +# 格式:频道名,URL +CCTV-1,http://example.com/cctv1.m3u8 +CCTV-2,http://example.com/cctv2.m3u8 +CCTV-3,http://example.com/cctv3.m3u8 +CCTV-4,http://example.com/cctv4.m3u8 +CCTV-5,http://example.com/cctv5.m3u8 +CCTV-6,http://example.com/cctv6.m3u8 +CCTV-7,http://example.com/cctv7.m3u8 +CCTV-8,http://example.com/cctv8.m3u8 +CCTV-9,http://example.com/cctv9.m3u8 +CCTV-10,http://example.com/cctv10.m3u8 +CCTV-11,http://example.com/cctv11.m3u8 +CCTV-12,http://example.com/cctv12.m3u8 +CCTV-13,http://example.com/cctv13.m3u8 +CCTV-14,http://example.com/cctv14.m3u8 +CCTV-15,http://example.com/cctv15.m3u8 diff --git a/sources/local.txt b/sources/local.txt new file mode 100644 index 0000000..77b707c --- /dev/null +++ b/sources/local.txt @@ -0,0 +1,7 @@ +# 地方台源 +# 格式:频道名,URL +杭州综合,http://example.com/hangzhou.m3u8 +南京新闻,http://example.com/nanjing.m3u8 +成都新闻,http://example.com/chengdu.m3u8 +武汉新闻,http://example.com/wuhan.m3u8 +广州综合,http://example.com/guangzhou.m3u8 diff --git a/sources/satellite.txt b/sources/satellite.txt new file mode 100644 index 0000000..f25a182 --- /dev/null +++ b/sources/satellite.txt @@ -0,0 +1,12 @@ +# 卫视频道源 +# 格式:频道名,URL +湖南卫视,http://example.com/hunan.m3u8 +浙江卫视,http://example.com/zhejiang.m3u8 +江苏卫视,http://example.com/jiangsu.m3u8 +东方卫视,http://example.com/dongfang.m3u8 +北京卫视,http://example.com/beijing.m3u8 +广东卫视,http://example.com/guangdong.m3u8 +深圳卫视,http://example.com/shenzhen.m3u8 +安徽卫视,http://example.com/anhui.m3u8 +山东卫视,http://example.com/shandong.m3u8 +天津卫视,http://example.com/tianjin.m3u8