From 7a6618e546c6d11b583671033133b3bffe98ff9b Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 28 Sep 2026 22:26:11 +0800 Subject: [PATCH] =?UTF-8?q?AI=20=E7=94=9F=E6=88=90:=20=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=9E=84=E5=BB=BA=20APTV=20=E7=9B=B4?= =?UTF-8?q?=E6=92=AD=E6=BA=90=E7=9A=84=E8=84=9A=E6=9C=AC=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=EF=BC=8C=E6=AF=8F=E5=A4=A9=E8=87=AA=E5=8A=A8=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=A4=AE=E8=A7=86=E3=80=81=E5=8D=AB=E8=A7=86=E3=80=81=E5=9C=B0?= =?UTF-8?q?=E6=96=B9=E5=8F=B0=E9=A2=91=E9=81=93=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=BA=90=E8=BF=87=E6=BB=A4=E3=80=81=E5=8E=BB=E9=87=8D=E5=92=8C?= =?UTF-8?q?=E5=8F=AF=E7=94=A8=E6=80=A7=E6=A3=80=E6=B5=8B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/daily-update.yml | 32 +++++++++++ README.md | 85 ++++++++++++++++++++++++++++++ config.py | 23 ++++++++ fetcher.py | 34 ++++++++++++ filter.py | 56 ++++++++++++++++++++ generator.py | 30 +++++++++++ main.py | 54 +++++++++++++++++++ requirements.txt | 1 + sources/cctv.txt | 17 ++++++ sources/local.txt | 7 +++ sources/satellite.txt | 12 +++++ 11 files changed, 351 insertions(+) create mode 100644 .github/workflows/daily-update.yml create mode 100644 README.md create mode 100644 config.py create mode 100644 fetcher.py create mode 100644 filter.py create mode 100644 generator.py create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 sources/cctv.txt create mode 100644 sources/local.txt create mode 100644 sources/satellite.txt 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