f26d4d8f7f
- Multi-source content aggregation (Bilibili/YouTube/Zhihu) - Smart inbox management with AI quality scoring - Long video chunking analysis (30min+ auto-segmentation) - Topic-based organization - Git auto-sync to Obsidian - Template system with Jinja2 and fallback rendering - CLI commands: subscribe, inbox, process, skip, status, search - Comprehensive test suite with unit and integration tests - Documentation: README.md and SKILL.md Checkpoint 7-8 completed by Agent E
169 lines
5.9 KiB
Python
169 lines
5.9 KiB
Python
"""Obsidian KB - 存储管理"""
|
|
|
|
import os
|
|
import json
|
|
from pathlib import Path
|
|
from typing import List, Optional, Dict, Any
|
|
from datetime import datetime
|
|
|
|
from models import Content, ContentStatus
|
|
from config import get_config
|
|
|
|
|
|
class Storage:
|
|
"""JSON文件存储管理"""
|
|
|
|
def __init__(self, base_path: Optional[str] = None):
|
|
config = get_config()
|
|
self.base_path = base_path or config.obsidian_path
|
|
self.inbox_path = os.path.join(self.base_path, 'Inbox')
|
|
self.topics_path = os.path.join(self.base_path, 'Topics')
|
|
self._ensure_dirs()
|
|
|
|
def _ensure_dirs(self) -> None:
|
|
"""确保目录存在"""
|
|
os.makedirs(self.inbox_path, exist_ok=True)
|
|
os.makedirs(self.topics_path, exist_ok=True)
|
|
|
|
def _content_file_path(self, content_id: str) -> str:
|
|
"""获取内容文件路径"""
|
|
return os.path.join(self.inbox_path, f'{content_id}.json')
|
|
|
|
def _topic_index_path(self, topic: str) -> str:
|
|
"""获取主题索引路径"""
|
|
topic_dir = os.path.join(self.topics_path, topic)
|
|
os.makedirs(topic_dir, exist_ok=True)
|
|
return os.path.join(topic_dir, 'index.json')
|
|
|
|
# ===== Inbox 管理 =====
|
|
|
|
def save_content(self, content: Content) -> None:
|
|
"""保存内容到Inbox"""
|
|
file_path = self._content_file_path(content.id)
|
|
with open(file_path, 'w', encoding='utf-8') as f:
|
|
json.dump(content.to_dict(), f, ensure_ascii=False, indent=2)
|
|
|
|
def load_content(self, content_id: str) -> Optional[Content]:
|
|
"""加载指定内容"""
|
|
file_path = self._content_file_path(content_id)
|
|
if not os.path.exists(file_path):
|
|
return None
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
return Content.from_dict(data)
|
|
|
|
def delete_content(self, content_id: str) -> bool:
|
|
"""删除内容"""
|
|
file_path = self._content_file_path(content_id)
|
|
if os.path.exists(file_path):
|
|
os.remove(file_path)
|
|
return True
|
|
return False
|
|
|
|
def list_inbox(self, status: Optional[ContentStatus] = None) -> List[Content]:
|
|
"""列出Inbox中的内容"""
|
|
contents = []
|
|
if not os.path.exists(self.inbox_path):
|
|
return contents
|
|
|
|
for filename in os.listdir(self.inbox_path):
|
|
if filename.endswith('.json'):
|
|
file_path = os.path.join(self.inbox_path, filename)
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
content = Content.from_dict(data)
|
|
if status is None or content.status == status:
|
|
contents.append(content)
|
|
|
|
# 按创建时间排序
|
|
contents.sort(key=lambda x: x.created_at, reverse=True)
|
|
return contents
|
|
|
|
def get_pending_count(self) -> int:
|
|
"""获取待处理内容数量"""
|
|
return len(self.list_inbox(ContentStatus.CONFIRMED))
|
|
|
|
def get_stats(self) -> Dict[str, int]:
|
|
"""获取统计信息"""
|
|
all_contents = self.list_inbox()
|
|
stats = {status.value: 0 for status in ContentStatus}
|
|
for content in all_contents:
|
|
stats[content.status.value] += 1
|
|
return stats
|
|
|
|
# ===== Topics 管理 =====
|
|
|
|
def add_to_topic(self, content: Content, topic: str) -> None:
|
|
"""将内容添加到主题"""
|
|
index_path = self._topic_index_path(topic)
|
|
|
|
# 读取现有索引
|
|
entries = []
|
|
if os.path.exists(index_path):
|
|
with open(index_path, 'r', encoding='utf-8') as f:
|
|
entries = json.load(f)
|
|
|
|
# 添加新条目
|
|
entry = {
|
|
'id': content.id,
|
|
'title': content.title,
|
|
'url': content.url,
|
|
'added_at': datetime.now().isoformat(),
|
|
'output_path': content.output_path
|
|
}
|
|
|
|
# 避免重复
|
|
entries = [e for e in entries if e['id'] != content.id]
|
|
entries.append(entry)
|
|
|
|
# 保存索引
|
|
with open(index_path, 'w', encoding='utf-8') as f:
|
|
json.dump(entries, f, ensure_ascii=False, indent=2)
|
|
|
|
def get_topic_contents(self, topic: str) -> List[Dict[str, Any]]:
|
|
"""获取主题下的所有内容"""
|
|
index_path = self._topic_index_path(topic)
|
|
if not os.path.exists(index_path):
|
|
return []
|
|
with open(index_path, 'r', encoding='utf-8') as f:
|
|
return json.load(f)
|
|
|
|
def list_topics(self) -> List[str]:
|
|
"""列出所有主题"""
|
|
if not os.path.exists(self.topics_path):
|
|
return []
|
|
return [d for d in os.listdir(self.topics_path)
|
|
if os.path.isdir(os.path.join(self.topics_path, d))]
|
|
|
|
def search(self, query: str) -> List[Content]:
|
|
"""搜索内容"""
|
|
results = []
|
|
all_contents = self.list_inbox()
|
|
query_lower = query.lower()
|
|
|
|
for content in all_contents:
|
|
if (query_lower in content.title.lower() or
|
|
query_lower in content.summary.lower() or
|
|
query_lower in content.author.lower() or
|
|
any(query_lower in tag.lower() for tag in content.tags)):
|
|
results.append(content)
|
|
|
|
return results
|
|
|
|
def move_to_archive(self, content_id: str) -> bool:
|
|
"""将内容移动到归档"""
|
|
content = self.load_content(content_id)
|
|
if not content:
|
|
return False
|
|
|
|
archive_path = os.path.join(self.base_path, 'Archive')
|
|
os.makedirs(archive_path, exist_ok=True)
|
|
|
|
source_file = self._content_file_path(content_id)
|
|
target_file = os.path.join(archive_path, f'{content_id}.json')
|
|
|
|
if os.path.exists(source_file):
|
|
os.rename(source_file, target_file)
|
|
return True
|
|
return False
|