diff --git a/main.py b/main.py index bbb0af3..c538d30 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,4 @@ +import base64 import os import re import smtplib @@ -41,28 +42,36 @@ def _extract_urls(summary): return urls, decoded -def _pick_url(urls, mode): - if mode == "v2ray": - for suffix in (".txt", ".json"): - for url in urls: - if url.lower().endswith(suffix): - return url - if mode == "clash": - for suffix in (".yaml", ".yml"): - for url in urls: - if url.lower().endswith(suffix): - return url - return "" +_NODE_SCHEME_RE = re.compile(r"(?:vmess|vless|trojan|ss|ssr|hysteria2?|tuic)://") -def _pick_urls(urls, mode): - matched = [] - suffixes = (".txt", ".json") if mode == "v2ray" else (".yaml", ".yml") - for suffix in suffixes: - for url in urls: - if url.lower().endswith(suffix) and url not in matched: - matched.append(url) - return matched +def _b64decode(text): + compact = re.sub(r"\s+", "", text) + if not compact: + return "" + try: + # binascii.Error 是 ValueError 的子类,统一捕获即可 + raw = base64.b64decode(compact + "=" * (-len(compact) % 4)) + except ValueError: + return "" + return raw.decode("utf-8", "ignore") + + +def _detect_kind(text): + """根据下载内容判断订阅类型:'clash' / 'v2ray',无法识别返回 None。""" + sample = text.strip() + if not sample: + return None + # clash 配置为 YAML,包含 proxies/proxy-groups 字段 + if re.search(r"^(?:proxies|proxy-groups)\s*:", sample, re.MULTILINE): + return "clash" + # v2ray 订阅为节点 URI 列表,可能是明文或 base64 编码 + if _NODE_SCHEME_RE.search(sample): + return "v2ray" + decoded = _b64decode(sample) + if decoded and _NODE_SCHEME_RE.search(decoded): + return "v2ray" + return None def _download_with_retry(urls): @@ -108,19 +117,25 @@ def _build_session(): return session -def _download_candidates(session, urls): - if not urls: - return None, None +def _classify_subscriptions(session, urls): + """逐个下载候选链接并按内容判断类型,返回 {'v2ray': (req, url), 'clash': (req, url)}。""" + found = {} for url in urls: + if "v2ray" in found and "clash" in found: + break try: req = session.get(url, verify=False, timeout=20) except requests.RequestException as e: write_log(f"请求失败:{url} - {e}", "WARN") continue - if req.status_code in ok_code: - return req, url - write_log(f"请求失败:{url} - {req.status_code}", "WARN") - return None, urls[0] + if req.status_code not in ok_code: + write_log(f"请求失败:{url} - {req.status_code}", "WARN") + continue + kind = _detect_kind(req.text) + if kind and kind not in found: + found[kind] = (req, url) + write_log(f"识别到 {kind} 订阅:{url}", "INFO") + return found def get_subscribe_url(): dirs = './subscribe' @@ -161,62 +176,40 @@ def get_subscribe_url(): write_log("暂时没有可用的订阅更新", "WARN") return - urls, decoded_summary = _extract_urls(summary) + urls, _ = _extract_urls(summary) - v2ray_url = _pick_url(urls, "v2ray") - clash_url = _pick_url(urls, "clash") - v2ray_candidates = _pick_urls(urls, "v2ray") - clash_candidates = _pick_urls(urls, "clash") - - # 兼容旧页面结构,通用提取失败时再尝试历史规则 - if not v2ray_url: - v2ray_list = re.findall(r">V2Ray/XRay -> (.*?)", summary) - if not v2ray_list: - v2ray_list = re.findall(r">V2Ray/XRay -> (.*?)", decoded_summary) - if any(v2ray_list): - v2ray_url = v2ray_list[-1].replace('amp;', '') - if v2ray_url not in v2ray_candidates: - v2ray_candidates.append(v2ray_url) - - if not clash_url: - clash_list = re.findall(r">clash -> (.*?)", summary) - if not clash_list: - clash_list = re.findall(r">clash -> (.*?)", decoded_summary) - if any(clash_list) and not clash_list[-1].startswith("订阅地址生成失败"): - clash_url = clash_list[-1].replace('amp;', '') - if clash_url not in clash_candidates: - clash_candidates.append(clash_url) + # 链接已无固定后缀,需下载内容后再判断是 v2ray 还是 clash + classified = _classify_subscriptions(session, urls) # 获取普通订阅链接 - if v2ray_url: - v2ray_req, used_v2ray_url = _download_candidates(session, v2ray_candidates) - if not v2ray_req: - cache_file = dirs + '/v2ray.txt' - if os.path.exists(cache_file) and os.path.getsize(cache_file) > 0: - update_list.append("v2ray: cache") - write_log(f"获取 v2ray 订阅失败,已保留本地缓存:{used_v2ray_url}", "WARN") - else: - write_log(f"获取 v2ray 订阅失败:{used_v2ray_url}", "WARN") + v2ray_entry = classified.get("v2ray") + if v2ray_entry: + v2ray_req, _ = v2ray_entry + update_list.append(f"v2ray: {v2ray_req.status_code}") + with open(dirs + '/v2ray.txt', 'w', encoding="utf-8") as f: + f.write(v2ray_req.text) + else: + cache_file = dirs + '/v2ray.txt' + if os.path.exists(cache_file) and os.path.getsize(cache_file) > 0: + update_list.append("v2ray: cache") + write_log("未获取到 v2ray 订阅,已保留本地缓存", "WARN") else: - update_list.append(f"v2ray: {v2ray_req.status_code}") - with open(dirs + '/v2ray.txt', 'w', encoding="utf-8") as f: - f.write(v2ray_req.text) + write_log("未获取到 v2ray 订阅", "WARN") # 获取clash订阅链接 - if clash_url and not clash_url.startswith("订阅地址生成失败"): - clash_req, used_clash_url = _download_candidates(session, clash_candidates) - if not clash_req: - cache_file = dirs + '/clash.yml' - if os.path.exists(cache_file) and os.path.getsize(cache_file) > 0: - update_list.append("clash: cache") - write_log(f"获取 clash 订阅失败,已保留本地缓存:{used_clash_url}", "WARN") - else: - write_log(f"获取 clash 订阅失败:{used_clash_url}", "WARN") + clash_entry = classified.get("clash") + if clash_entry: + clash_req, _ = clash_entry + update_list.append(f"clash: {clash_req.status_code}") + with open(dirs + '/clash.yml', 'w', encoding="utf-8") as f: + f.write(clash_req.content.decode("utf-8")) + else: + cache_file = dirs + '/clash.yml' + if os.path.exists(cache_file) and os.path.getsize(cache_file) > 0: + update_list.append("clash: cache") + write_log("未获取到 clash 订阅,已保留本地缓存", "WARN") else: - update_list.append(f"clash: {clash_req.status_code}") - with open(dirs + '/clash.yml', 'w', encoding="utf-8") as f: - clash_content = clash_req.content.decode("utf-8") - f.write(clash_content) + write_log("未获取到 clash 订阅", "WARN") if update_list: file_pat = re.compile(r"v2ray\.txt|clash\.yml") if file_pat.search(os.popen("git status").read()):