Files
iptv/test.py
T
2026-02-07 10:30:02 +08:00

268 lines
8.3 KiB
Python

import requests
import re
import json
import subprocess
import shlex
import os
import sys
# ================= 配置 =================
CONFIG_FILE = "upstreams.json"
OUTPUT_FILE = "channels.json"
# ================= 功能函数 =================
def get_stream_data(url):
"""
获取媒体详细信息
返回字典包含: width, height, bitrate, video, audio, fps, rate
"""
data = {
"width": 0,
"height": 0,
"bitrate": 0,
"video": "",
"audio": "",
"fps": 0,
"rate": 0
}
has_video = False
has_audio = False
try:
# 8秒超时检测
cmd = f'ffprobe -v quiet -print_format json -show_format -show_streams "{url}"'
result = subprocess.run(shlex.split(cmd), capture_output=True, text=True, timeout=8)
if result.returncode != 0:
return None
info = json.loads(result.stdout)
if "streams" not in info or not info["streams"]:
return None
for stream in info["streams"]:
codec_type = stream.get("codec_type")
# --- 视频流 ---
if codec_type == "video" and not has_video:
has_video = True
data["width"] = stream.get("width", 0)
data["height"] = stream.get("height", 0)
data["video"] = stream.get("codec_name", "unknown")
r_rate = stream.get("r_frame_rate", "0/0")
if "/" in r_rate:
try:
num, den = map(int, r_rate.split('/'))
if den > 0:
data["fps"] = round(num / den, 2)
except: pass
else:
try:
data["fps"] = float(r_rate)
except: pass
if stream.get("bit_rate"):
data["bitrate"] = int(stream.get("bit_rate"))
# --- 音频流 ---
elif codec_type == "audio" and not has_audio:
has_audio = True
data["audio"] = stream.get("codec_name", "unknown")
sample_rate = stream.get("sample_rate")
if sample_rate:
try:
data["rate"] = int(sample_rate)
except: pass
if data["bitrate"] == 0 and stream.get("bit_rate"):
data["bitrate"] = int(stream.get("bit_rate"))
# 有效性判定
if not has_video and not has_audio:
return None
# 码率兜底
if data["bitrate"] == 0 and "format" in info:
fmt_bitrate = info["format"].get("bit_rate")
if fmt_bitrate:
try:
data["bitrate"] = int(fmt_bitrate)
except: pass
except Exception:
return None
return data
def check_keywords(targets, keywords):
"""
检查是否命中关键词 (targets: [url, name, group])
"""
for target in targets:
if not target: continue
for kw in keywords:
if kw in target:
return True
return False
def parse_m3u_content(content, upstream_config, seen_urls):
"""
解析 M3U 内容
upstream_config: 包含 url, tag, whitelist, blacklist, replace 等信息的字典
"""
results = []
lines = content.splitlines()
current_meta = {}
# 提取配置信息
tag = upstream_config.get('tag', '')
whitelist = upstream_config.get('whitelist', [])
blacklist = upstream_config.get('blacklist', [])
replace_rules = upstream_config.get('replace', {})
for line in lines:
line = line.strip()
if not line:
continue
if line.startswith("#EXTINF"):
current_meta = {}
# 提取所有属性 key="value"
attributes = re.findall(r'([a-zA-Z0-9-]+)="([^"]*)"', line)
for key, value in attributes:
current_meta[key.lower()] = value
# 提取频道名称
last_comma = line.rfind(',')
if last_comma != -1:
current_meta['name_raw'] = line[last_comma+1:].strip()
else:
current_meta['name_raw'] = ""
elif not line.startswith("#"):
url = line
# --- 1. 预处理 ---
name = current_meta.get('name_raw', 'Unknown')
group = current_meta.get('group-title', '未分类')
# --- 2. 替换 ---
if replace_rules:
for k, v in replace_rules.items():
url = url.replace(k, v)
# --- 3. 过滤 (White/Black List) ---
check_targets = [url, name, group]
if whitelist and len(whitelist) > 0:
# 存在白名单:必须命中才保留
if not check_keywords(check_targets, whitelist):
continue
elif blacklist and len(blacklist) > 0:
# 仅存在黑名单:命中则丢弃
if check_keywords(check_targets, blacklist):
continue
# --- 4. 去重 ---
if url in seen_urls:
continue
seen_urls.add(url)
# --- 5. 名称 ---
tvg = current_meta.get('tvg-name') or current_meta.get('tvg-id') or name
print(f"检测 [{tag}]: {name} | {url}", end="", flush=True)
# --- 6. FFprobe 检测 ---
s_data = get_stream_data(url)
if s_data is None:
print(" -> [无效] PASS")
continue
# --- 7. 输出与保存 ---
out_info = []
if s_data['width'] > 0:
out_info.append(f"视频:{s_data['video']} {s_data['width']}x{s_data['height']}")
else:
out_info.append("视频:无")
if s_data['audio']:
out_info.append(f"音频:{s_data['audio']}")
print(f" -> [OK] {'|'.join(out_info)}")
channel_obj = {
"tag": tag, # 新增: 来源标签
"group": group,
"tvg": tvg,
"name": name,
"url": url,
"width": s_data['width'],
"height": s_data['height'],
"bitrate": s_data['bitrate'],
"video": s_data['video'],
"audio": s_data['audio'],
"fps": s_data['fps'],
"rate": s_data['rate']
}
results.append(channel_obj)
return results
def load_upstreams(filepath):
"""读取配置文件"""
if not os.path.exists(filepath):
print(f"错误: 找不到配置文件 {filepath}")
return []
try:
with open(filepath, 'r', encoding='utf-8') as f:
return json.load(f)
except json.JSONDecodeError as e:
print(f"错误: 配置文件格式不正确 (JSON解析失败) - {e}")
return []
def main():
# 1. 读取外部 JSON 配置
upstreams = load_upstreams(CONFIG_FILE)
if not upstreams:
print("未加载到有效配置,程序退出。")
sys.exit(1)
all_data = []
global_seen = set()
# 2. 遍历处理
for upstream in upstreams:
url = upstream.get('url')
if not url: continue
print(f"\n--- 正在处理源 [{upstream.get('tag', '无标签')}]: {url} ---")
try:
resp = requests.get(url, timeout=10)
resp.encoding = 'utf-8'
if resp.status_code == 200:
# 将整个 upstream 配置传进去,方便提取 tag/whitelist/blacklist 等
data = parse_m3u_content(resp.text, upstream, global_seen)
all_data.extend(data)
else:
print(f"HTTP 错误: {resp.status_code}")
except Exception as e:
print(f"下载异常: {e}")
# 3. 保存结果
print(f"\n处理完成,共保存 {len(all_data)} 个有效频道到 {OUTPUT_FILE}")
with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
json.dump(all_data, f, ensure_ascii=False, indent=4)
if __name__ == "__main__":
main()