Files

116 lines
3.5 KiB
Python

"""配置加载和源管理"""
import fnmatch
import json
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Dict, List
from urllib.parse import urlparse
def _get_local_timezone() -> timezone:
"""自动检测本地时区"""
return datetime.now().astimezone().tzinfo
def get_timezone(config: Dict = None) -> timezone:
"""
获取配置时区,用于推送消息展示本地化时间
读取信息源统一使用 UTC 时间
如果 config 中没有 timezone_hours,则自动检测本地时区
"""
if config is None:
try:
config = load_config()
except Exception:
return _get_local_timezone()
hours = config.get("schedule", {}).get("timezone_hours")
if hours is None:
return _get_local_timezone()
return timezone(timedelta(hours=hours))
# 向后兼容的别名
def get_cst(config: Dict = None) -> timezone:
"""向后兼容,使用 get_timezone"""
return get_timezone(config)
def load_config(config_path: str = "config.json") -> Dict:
"""加载配置文件"""
path = Path(config_path)
if not path.exists():
raise FileNotFoundError(f"配置文件不存在: {config_path}")
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def parse_opml(opml_path: str) -> List[Dict]:
"""解析OPML文件获取订阅源列表"""
path = Path(opml_path)
if not path.exists():
return []
tree = ET.parse(path)
root = tree.getroot()
feeds = []
for outline in root.findall(".//outline[@type='rss']"):
feeds.append({
"title": outline.get("title", ""),
"xmlUrl": outline.get("xmlUrl", ""),
"category": outline.get("category", "未分类"),
})
return feeds
def merge_sources(sources_config: Dict) -> List[Dict]:
"""合并base_opml + add - block,以xmlUrl为key去重"""
# 1. 解析base OPML
base = parse_opml(sources_config.get("base_opml", ""))
# 2. 添加自定义源
add_list = sources_config.get("add", [])
all_sources = base + add_list
# 3. 应用block (以xmlUrl匹配)
block_list = sources_config.get("block", [])
block_urls = {b.get("xmlUrl", "") for b in block_list}
filtered = [s for s in all_sources if s.get("xmlUrl", "") not in block_urls]
# 4. 应用block_domains (域名屏蔽,支持通配符 *.substack.com)
block_domains = sources_config.get("block_domains", [])
if block_domains:
def is_domain_blocked(url: str) -> bool:
try:
domain = urlparse(url).netloc.lower()
for pattern in block_domains:
# 转换通配符模式为匹配格式
if pattern.startswith("*."):
# *.substack.com 匹配 substack.com 和 addyo.substack.com
suffix = pattern[2:] # substack.com
if domain == suffix or domain.endswith("." + suffix):
return True
elif fnmatch.fnmatch(domain, pattern):
return True
return False
except Exception:
return False
filtered = [s for s in filtered if not is_domain_blocked(s.get("xmlUrl", ""))]
# 5. 去重 (以xmlUrl为key)
seen = set()
result = []
for s in filtered:
url = s.get("xmlUrl", "")
if url and url not in seen:
seen.add(url)
result.append(s)
return result