AI 生成: 一个自动构建 APTV 直播源的脚本工具,每天自动更新央视、卫视、地方台频道,支持源过滤、去重和可用性检测。
This commit is contained in:
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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 = ["央视", "卫视", "地方台"]
|
||||||
+34
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
requests>=2.31.0
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user