添加订阅分类,支持分类导出
This commit is contained in:
@@ -10,8 +10,9 @@ Usage:
|
||||
python3 blogwatcher-daily.py --update 3 --all # 强制回扫指定订阅最新 10 篇
|
||||
|
||||
# 订阅管理
|
||||
python3 blogwatcher-daily.py --list # 列出所有订阅
|
||||
python3 blogwatcher-daily.py --add "频道名" "RSS URL" # 添加订阅
|
||||
python3 blogwatcher-daily.py --list # 列出所有订阅(按分类分组)
|
||||
python3 blogwatcher-daily.py --add "频道名" "RSS URL" # 添加订阅(默认分类=未分类)
|
||||
python3 blogwatcher-daily.py --add "频道名" "RSS URL" --category "AI技术" # 添加订阅并指定分类
|
||||
|
||||
# 查看文章
|
||||
python3 blogwatcher-daily.py --articles "频道名" # 列出该订阅所有文章
|
||||
@@ -26,10 +27,16 @@ Usage:
|
||||
python3 blogwatcher-daily.py --delete 3 --article-id 2 # 删除该订阅下第 2 篇(1-based,顺序同 --articles)
|
||||
|
||||
# 导出
|
||||
python3 blogwatcher-daily.py --export markdown # 把今天的文章导出为 Markdown
|
||||
python3 blogwatcher-daily.py --export html --date 2026-08-28
|
||||
python3 blogwatcher-daily.py --export txt --limit 20
|
||||
python3 blogwatcher-daily.py --export markdown # 今天的文章
|
||||
python3 blogwatcher-daily.py --export html --date 2026-08-28 # 指定日期
|
||||
python3 blogwatcher-daily.py --export txt --limit 20 # 全库最新 20 篇
|
||||
python3 blogwatcher-daily.py --export markdown --sub "异次元软件世界" --limit 20 # 单订阅最新 20 篇
|
||||
python3 blogwatcher-daily.py --export markdown --category "AI技术" # 分类全部文章
|
||||
python3 blogwatcher-daily.py --export markdown --category "AI技术" --date $(date +%F) # 分类今日文章
|
||||
|
||||
订阅文件格式(subscriptions.txt):
|
||||
新格式(推荐):`分类|频道名|URL`
|
||||
旧格式(仍兼容):`频道名|URL`(自动归为 `未分类`)
|
||||
|
||||
订阅标识(SUB):
|
||||
可传 --list 输出中的整数序号(如 3),也可传订阅名(精确 → 忽略大小写精确 → 忽略大小写包含)。
|
||||
@@ -217,47 +224,116 @@ def parse_rss(xml_content, fetch_url):
|
||||
|
||||
# ========== 订阅管理 ==========
|
||||
def load_subscriptions():
|
||||
"""从文件加载订阅列表"""
|
||||
"""从文件加载订阅列表
|
||||
|
||||
支持两种格式(同一文件内可混用,脚本自动识别):
|
||||
- 新格式:`category|name|url[|enabled]`
|
||||
- 旧格式:`name|url[|enabled]`(分类默认为 `未分类`)
|
||||
|
||||
判定规则:若 `parts[1]` 以 `http://` / `https://` 开头,视为旧格式;
|
||||
否则视为新格式(`parts[2]` 必须是 URL)。
|
||||
"""
|
||||
subs = []
|
||||
if os.path.exists(SUBSCRIPTIONS_FILE):
|
||||
with open(SUBSCRIPTIONS_FILE, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#'):
|
||||
parts = line.split('|')
|
||||
if len(parts) >= 2:
|
||||
name = parts[0].strip()
|
||||
url = parts[1].strip()
|
||||
enabled = parts[2].strip() != '0' if len(parts) > 2 else True
|
||||
if enabled:
|
||||
subs.append({'name': name, 'url': url})
|
||||
if not os.path.exists(SUBSCRIPTIONS_FILE):
|
||||
return subs
|
||||
|
||||
with open(SUBSCRIPTIONS_FILE, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
parts = [p.strip() for p in line.split('|')]
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
|
||||
if parts[1].startswith(('http://', 'https://')):
|
||||
category = '未分类'
|
||||
name = parts[0]
|
||||
url = parts[1]
|
||||
enabled_str = parts[2] if len(parts) > 2 else None
|
||||
elif len(parts) >= 3 and parts[2].startswith(('http://', 'https://')):
|
||||
category = parts[0]
|
||||
name = parts[1]
|
||||
url = parts[2]
|
||||
enabled_str = parts[3] if len(parts) > 3 else None
|
||||
else:
|
||||
continue
|
||||
|
||||
enabled = enabled_str != '0' if enabled_str is not None else True
|
||||
if enabled:
|
||||
subs.append({'category': category, 'name': name, 'url': url})
|
||||
return subs
|
||||
|
||||
def save_subscription(name, url):
|
||||
def save_subscription(name, url, category='未分类'):
|
||||
"""添加订阅到文件"""
|
||||
# 检查是否已存在
|
||||
subs = load_subscriptions()
|
||||
for sub in subs:
|
||||
if sub['url'] == url:
|
||||
print(f"⚠️ 订阅已存在: {name}")
|
||||
return False
|
||||
|
||||
with open(SUBSCRIPTIONS_FILE, 'a') as f:
|
||||
f.write(f"{name}|{url}\n")
|
||||
print(f"✅ 已添加订阅: {name}")
|
||||
|
||||
with open(SUBSCRIPTIONS_FILE, 'a', encoding='utf-8') as f:
|
||||
f.write(f"{category}|{name}|{url}\n")
|
||||
print(f"✅ 已添加订阅: [{category}] {name}")
|
||||
return True
|
||||
|
||||
def list_subscriptions():
|
||||
"""列出所有订阅"""
|
||||
"""列出所有订阅(按分类分组,全局 1-based 序号保持不变)"""
|
||||
subs = load_subscriptions()
|
||||
if not subs:
|
||||
print("📭 暂无订阅")
|
||||
return
|
||||
|
||||
print(f"\n📡 当前订阅 ({len(subs)} 个):\n")
|
||||
|
||||
grouped = {}
|
||||
order = []
|
||||
for i, sub in enumerate(subs, 1):
|
||||
print(f" [{i}] {sub['name']}")
|
||||
print(f" {sub['url']}\n")
|
||||
cat = sub.get('category', '未分类')
|
||||
if cat not in grouped:
|
||||
grouped[cat] = []
|
||||
order.append(cat)
|
||||
grouped[cat].append((i, sub))
|
||||
|
||||
print(f"\n📡 当前订阅 ({len(subs)} 个 · {len(order)} 个分类):\n")
|
||||
for cat in order:
|
||||
items = grouped[cat]
|
||||
print(f"── 【{cat}】({len(items)} 个) ──")
|
||||
for i, sub in items:
|
||||
print(f" [{i}] {sub['name']}")
|
||||
print(f" {sub['url']}")
|
||||
print()
|
||||
|
||||
|
||||
def find_subscriptions_by_category(category):
|
||||
"""按分类查找所有订阅。
|
||||
匹配顺序:精确 → 忽略大小写精确 → 忽略大小写子串。
|
||||
匹配到多个分类时打印警告,仍返回全部结果(导出场景下允许跨相邻分类合并)。
|
||||
"""
|
||||
subs = load_subscriptions()
|
||||
if not subs:
|
||||
print("❌ 订阅列表为空", file=sys.stderr)
|
||||
return []
|
||||
|
||||
exact = [s for s in subs if s.get('category', '未分类') == category]
|
||||
if exact:
|
||||
return exact
|
||||
|
||||
lower = category.lower()
|
||||
exact_ci = [s for s in subs if s.get('category', '未分类').lower() == lower]
|
||||
if exact_ci:
|
||||
return exact_ci
|
||||
|
||||
substr = [s for s in subs if lower in s.get('category', '未分类').lower()]
|
||||
if substr:
|
||||
matched_cats = sorted({s['category'] for s in substr})
|
||||
if len(matched_cats) > 1:
|
||||
print(f"⚠️ '{category}' 子串匹配到多个分类: {matched_cats}", file=sys.stderr)
|
||||
return substr
|
||||
|
||||
print(f"❌ 未找到分类: {category}", file=sys.stderr)
|
||||
all_cats = sorted({s.get('category', '未分类') for s in subs})
|
||||
print(f" 可用分类: {all_cats}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
|
||||
def find_subscription(identifier):
|
||||
"""按序号(--list 中的 1-based 索引)或名称定位订阅。
|
||||
@@ -438,9 +514,9 @@ def delete_articles_for_subscription(sub, article_index=None):
|
||||
return True
|
||||
|
||||
# ========== 导出 ==========
|
||||
def fetch_articles(target_date=None, limit=None, feed_url=None):
|
||||
def fetch_articles(target_date=None, limit=None, feed_url=None, feed_urls=None):
|
||||
"""从 articles 表读取;可选按本地日期过滤(date(fetched_at,'localtime'))
|
||||
+ 可选按订阅 feed_url 过滤 + 可选限制数量。"""
|
||||
+ 可选按单订阅 feed_url 或多订阅 feed_urls 过滤 + 可选限制数量。"""
|
||||
if not os.path.exists(DB_PATH):
|
||||
print(f"❌ 数据库不存在: {DB_PATH}", file=sys.stderr)
|
||||
return []
|
||||
@@ -456,6 +532,10 @@ def fetch_articles(target_date=None, limit=None, feed_url=None):
|
||||
if feed_url:
|
||||
conditions.append("feed_url = ?")
|
||||
params.append(feed_url)
|
||||
if feed_urls:
|
||||
placeholders = ",".join("?" * len(feed_urls))
|
||||
conditions.append(f"feed_url IN ({placeholders})")
|
||||
params.extend(feed_urls)
|
||||
where = ("WHERE " + " AND ".join(conditions)) if conditions else ""
|
||||
|
||||
query = f"""
|
||||
@@ -556,13 +636,16 @@ FORMATTERS = {
|
||||
"html": format_html,
|
||||
}
|
||||
|
||||
def export_articles(fmt, target_date, limit, sub=None):
|
||||
def export_articles(fmt, target_date, limit, sub=None, category=None, category_subs=None):
|
||||
feed_url = sub['url'] if sub else None
|
||||
articles = fetch_articles(target_date, limit, feed_url=feed_url)
|
||||
feed_urls = [s['url'] for s in category_subs] if category_subs else None
|
||||
articles = fetch_articles(target_date, limit, feed_url=feed_url, feed_urls=feed_urls)
|
||||
|
||||
label_parts = []
|
||||
if sub:
|
||||
label_parts.append(sub['name'])
|
||||
if category:
|
||||
label_parts.append(f"分类: {category}")
|
||||
if target_date:
|
||||
label_parts.append(target_date)
|
||||
if limit:
|
||||
@@ -600,6 +683,9 @@ def main():
|
||||
help='导出的文章数上限;单独使用时忽略日期取全库最新 N 篇(仅 --export 有效)')
|
||||
parser.add_argument('--sub', metavar='SUB',
|
||||
help='与 --export 搭配:只导出指定订阅的文章(SUB=--list 序号或名称)')
|
||||
parser.add_argument('--category', metavar='CATEGORY',
|
||||
help='与 --export 搭配:只导出指定分类下所有订阅的文章;'
|
||||
'与 --add 搭配:为新订阅指定分类(默认 "未分类")')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -609,6 +695,10 @@ def main():
|
||||
parser.error("--article-id 必须与 --delete 一起使用")
|
||||
if args.sub and not args.export:
|
||||
parser.error("--sub 必须与 --export 一起使用")
|
||||
if args.category and not (args.export or args.add):
|
||||
parser.error("--category 必须与 --export 或 --add 一起使用")
|
||||
if args.sub and args.category and args.export:
|
||||
parser.error("--sub 与 --category 互斥(前者指定单个订阅,后者指定一组)")
|
||||
|
||||
global RSSHUB_BASE
|
||||
if args.rsshub:
|
||||
@@ -622,7 +712,7 @@ def main():
|
||||
if not url.startswith('http'):
|
||||
url = f"{RSSHUB_BASE}/{url}"
|
||||
stored_url = convert_to_stored_url(url)
|
||||
save_subscription(name, stored_url)
|
||||
save_subscription(name, stored_url, category=args.category or '未分类')
|
||||
|
||||
elif args.update:
|
||||
sub = find_subscription(args.update)
|
||||
@@ -660,17 +750,25 @@ def main():
|
||||
|
||||
elif args.export:
|
||||
sub = None
|
||||
category_subs = None
|
||||
category_name = None
|
||||
if args.sub:
|
||||
sub = find_subscription(args.sub)
|
||||
if not sub:
|
||||
sys.exit(1)
|
||||
if args.category:
|
||||
category_subs = find_subscriptions_by_category(args.category)
|
||||
if not category_subs:
|
||||
sys.exit(1)
|
||||
category_name = args.category
|
||||
if args.date:
|
||||
target_date = args.date
|
||||
elif args.limit or sub:
|
||||
elif args.limit or sub or category_subs:
|
||||
target_date = None
|
||||
else:
|
||||
target_date = date.today().isoformat()
|
||||
export_articles(args.export, target_date, args.limit, sub=sub)
|
||||
export_articles(args.export, target_date, args.limit,
|
||||
sub=sub, category=category_name, category_subs=category_subs)
|
||||
|
||||
else:
|
||||
scan_all(force_all=args.all)
|
||||
|
||||
Reference in New Issue
Block a user