From 5c86136912fa73ce9fe66f016c4236feae84b72c Mon Sep 17 00:00:00 2001 From: riglen Date: Thu, 9 Apr 2026 11:17:06 +0800 Subject: [PATCH] B --- .env.example | 3 +++ README.md | 3 +++ app/services/subscriptions.py | 42 ++++++++++++++++++++++++++++++++++- config/sources.yaml | 2 ++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 7c2e579..6341569 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,9 @@ PUBLIC_BASE_URL= # 1. Clash/Mihomo YAML proxies 文件 # 2. base64 编码 URI 订阅 # 3. 直接明文 URI 订阅 +# 也支持本地文件,例如: +# AIRPORT_B_URL=/app/data/sources/airport-b.txt +# AIRPORT_B_URL=file:///app/data/sources/airport-b.txt AIRPORT_A_URL= # 允许留空;留空时该机场会自动跳过,不参与默认查询 AIRPORT_B_URL= diff --git a/README.md b/README.md index 653fa63..9872189 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ sub-provider/ cn-ip.yaml data/ cache/ + sources/ .env.example Dockerfile docker-compose.yaml @@ -69,6 +70,7 @@ cp .env.example .env - `PUBLIC_PATH` 改成足够长的随机字符串 - `PUBLIC_BASE_URL` 建议填写你反代后的最终访问地址,例如 `https://sub.example.com` - `AIRPORT_A_URL` / `AIRPORT_B_URL` 都可以直接填订阅地址,项目会自动判断是 YAML 还是 URI 订阅 +- 也可以直接填本地文件路径,例如 `/app/data/sources/airport-b.txt` 或 `file:///app/data/sources/airport-b.txt` - 允许把其中一个留空;留空时这个机场会自动跳过 3. 启动: @@ -148,6 +150,7 @@ HEAD //bundle/stash.yaml?sources=airport-a,airport-b - 生成后的完整 YAML 会缓存到 `output/bundle-cache/`,默认 600 秒内直接返回缓存 - 可用 `force_refresh=true` 强制跳过缓存并覆盖旧缓存文件 - 上游订阅原始内容也会按 TTL 落盘缓存到 `data/fetch-cache/`,默认 900 秒 +- 如果你想固定使用本地文件订阅,可以把文件放到 `data/sources/`,再把 `AIRPORT_*_URL` 指向 `/app/data/sources/xxx.txt` --- diff --git a/app/services/subscriptions.py b/app/services/subscriptions.py index 714fe20..0aa39c7 100644 --- a/app/services/subscriptions.py +++ b/app/services/subscriptions.py @@ -4,13 +4,14 @@ import base64 import json import logging import re +from pathlib import Path from typing import Any, Iterable from urllib.parse import parse_qs, unquote, urlparse import httpx import yaml -from app.config import get_settings +from app.config import ROOT_DIR, get_settings from app.models import FetchResult, ProviderDocument, SourceConfig, SourceSnapshot from app.services.cache import TTLCache from app.services.fetch_cache import build_fetch_cache_key, load_fetch_cache, save_fetch_cache @@ -31,6 +32,17 @@ async def fetch_source(name: str, source: SourceConfig) -> FetchResult: logger.info("fetch_source memory cache hit: source=%s ttl=%s", name, ttl) return cached + local_path = resolve_local_source_path(source.url) + if local_path is not None: + logger.info("fetch_source local file: source=%s path=%s", name, local_path) + try: + result = FetchResult(text=local_path.read_text(encoding="utf-8"), headers={}) + except Exception: + logger.exception("fetch_source local file read failed: source=%s path=%s", name, local_path) + raise + _fetch_cache.set(name, result, ttl) + return result + cache_key = build_fetch_cache_key(name=name, url=source.url) disk_cached = load_fetch_cache( cache_dir=settings.fetch_cache_dir, @@ -66,6 +78,34 @@ async def fetch_source(name: str, source: SourceConfig) -> FetchResult: return result +def resolve_local_source_path(raw: str) -> Path | None: + candidate = raw.strip() + if not candidate: + return None + + parsed = urlparse(candidate) + if parsed.scheme in {"http", "https"}: + return None + + if parsed.scheme == "file": + path_text = unquote(parsed.path or "") + if parsed.netloc: + path_text = f"//{parsed.netloc}{path_text}" + if re.match(r"^/[A-Za-z]:", path_text): + path_text = path_text[1:] + path = Path(path_text) + else: + path = Path(candidate) + + if not path.is_absolute(): + path = ROOT_DIR / path + + resolved = path.resolve() + if not resolved.is_file(): + raise FileNotFoundError(f"Local source file not found: {resolved}") + return resolved + + async def build_provider_document(name: str, source: SourceConfig) -> ProviderDocument: settings = get_settings() ttl = source.cache_ttl_seconds or settings.cache_ttl_seconds diff --git a/config/sources.yaml b/config/sources.yaml index ca71295..0de40ab 100644 --- a/config/sources.yaml +++ b/config/sources.yaml @@ -15,6 +15,8 @@ sources: display_name: B kind: auto url: ${AIRPORT_B_URL} + headers: + User-Agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36" prefix: "[B] " include_regex: "" exclude_regex: "流量|重置|到期|续费|官网|离线|套餐"