fix: 复制 obsidian-kb 的 src 目录以解决依赖缺失
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
"""
|
||||
Obsidian Writer - 将内容写入 Obsidian vault
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional
|
||||
from ..models import Content
|
||||
|
||||
|
||||
class ObsidianWriter:
|
||||
"""Obsidian vault 写入器"""
|
||||
|
||||
def __init__(self, config):
|
||||
"""
|
||||
初始化写入器
|
||||
|
||||
Args:
|
||||
config: 配置对象
|
||||
"""
|
||||
self.config = config
|
||||
self.vault_path = config.vault_path
|
||||
|
||||
def write(self, data: Dict[str, Any], config) -> Optional[str]:
|
||||
"""
|
||||
将内容写入 Obsidian vault
|
||||
|
||||
Args:
|
||||
data: 内容数据字典或 Content 对象
|
||||
config: 配置对象
|
||||
|
||||
Returns:
|
||||
写入的文件路径,失败返回 None
|
||||
"""
|
||||
# 如果 data 是 Content 对象,转换为字典
|
||||
if isinstance(data, Content):
|
||||
content = data
|
||||
else:
|
||||
content = Content.from_dict(data)
|
||||
|
||||
# 构建文件路径
|
||||
file_path = self._build_file_path(content)
|
||||
|
||||
# 创建目录结构
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 生成 Markdown 内容
|
||||
markdown_content = self._generate_markdown(content)
|
||||
|
||||
# 写入文件
|
||||
try:
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
return str(file_path)
|
||||
except Exception as e:
|
||||
print(f"写入文件失败: {e}")
|
||||
return None
|
||||
|
||||
def _build_file_path(self, content: Content) -> Path:
|
||||
"""
|
||||
构建文件路径
|
||||
|
||||
格式: {vault}/videos/bilibili/{author}/{date}_{title}.md
|
||||
|
||||
Args:
|
||||
content: Content 对象
|
||||
|
||||
Returns:
|
||||
文件路径
|
||||
"""
|
||||
# 获取日期(从 created_at 提取日期部分)
|
||||
date_str = self._extract_date(content.created_at)
|
||||
|
||||
# 清理标题(移除特殊字符)
|
||||
safe_title = self._sanitize_filename(content.title)
|
||||
|
||||
# 构建文件名
|
||||
filename = f"{date_str}_{safe_title}.md"
|
||||
|
||||
# 构建完整路径
|
||||
# 格式: {vault}/videos/{source}/{author}/{filename}
|
||||
path_parts = [
|
||||
self.vault_path,
|
||||
'videos',
|
||||
content.source,
|
||||
self._sanitize_filename(content.author),
|
||||
filename
|
||||
]
|
||||
|
||||
return Path(*path_parts)
|
||||
|
||||
def _extract_date(self, iso_date: str) -> str:
|
||||
"""
|
||||
从 ISO 日期字符串提取日期部分
|
||||
|
||||
Args:
|
||||
iso_date: ISO 格式日期字符串
|
||||
|
||||
Returns:
|
||||
YYYY-MM-DD 格式日期字符串
|
||||
"""
|
||||
try:
|
||||
dt = datetime.fromisoformat(iso_date)
|
||||
return dt.strftime('%Y-%m-%d')
|
||||
except:
|
||||
# 如果解析失败,返回当前日期
|
||||
return datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
def _sanitize_filename(self, filename: str) -> str:
|
||||
"""
|
||||
清理文件名,移除不适合文件系统的字符
|
||||
|
||||
Args:
|
||||
filename: 原始文件名
|
||||
|
||||
Returns:
|
||||
清理后的文件名
|
||||
"""
|
||||
# 移除不适合文件系统的字符
|
||||
forbidden_chars = '<>:"/\\|?*'
|
||||
for char in forbidden_chars:
|
||||
filename = filename.replace(char, '')
|
||||
|
||||
# 移除首尾空格
|
||||
filename = filename.strip()
|
||||
|
||||
# 限制长度
|
||||
if len(filename) > 100:
|
||||
filename = filename[:100]
|
||||
|
||||
# 如果为空,使用默认值
|
||||
if not filename:
|
||||
filename = 'untitled'
|
||||
|
||||
return filename
|
||||
|
||||
def _generate_markdown(self, content: Content) -> str:
|
||||
"""
|
||||
生成 Obsidian Markdown 内容
|
||||
|
||||
格式:
|
||||
---
|
||||
title: xxx
|
||||
source: bilibili
|
||||
url: xxx
|
||||
author: xxx
|
||||
duration: xxx
|
||||
date: xxx
|
||||
tags: [xxx, xxx]
|
||||
---
|
||||
|
||||
# {title}
|
||||
|
||||
...内容...
|
||||
|
||||
Args:
|
||||
content: Content 对象
|
||||
|
||||
Returns:
|
||||
Markdown 内容字符串
|
||||
"""
|
||||
# 生成 frontmatter
|
||||
frontmatter = self._generate_frontmatter(content)
|
||||
|
||||
# 生成正文
|
||||
body = self._generate_body(content)
|
||||
|
||||
# 组合
|
||||
markdown = f"{frontmatter}\n{body}"
|
||||
|
||||
return markdown
|
||||
|
||||
def _generate_frontmatter(self, content: Content) -> str:
|
||||
"""
|
||||
生成 YAML frontmatter
|
||||
|
||||
Args:
|
||||
content: Content 对象
|
||||
|
||||
Returns:
|
||||
Frontmatter 字符串
|
||||
"""
|
||||
# 格式化时长
|
||||
duration_str = self._format_duration(content.duration) if content.duration else "N/A"
|
||||
|
||||
# 格式化日期
|
||||
date_str = self._extract_date(content.created_at)
|
||||
|
||||
# 构建标签列表
|
||||
tags = self._build_tags(content)
|
||||
tags_yaml = ", ".join([f'"{tag}"' for tag in tags])
|
||||
|
||||
frontmatter = f"""---
|
||||
title: {content.title}
|
||||
source: {content.source}
|
||||
url: {content.url}
|
||||
author: {content.author}
|
||||
duration: {duration_str}
|
||||
date: {date_str}
|
||||
tags: [{tags_yaml}]
|
||||
---"""
|
||||
|
||||
return frontmatter
|
||||
|
||||
def _build_tags(self, content: Content) -> list:
|
||||
"""
|
||||
构建标签列表
|
||||
|
||||
Args:
|
||||
content: Content 对象
|
||||
|
||||
Returns:
|
||||
标签列表
|
||||
"""
|
||||
tags = []
|
||||
|
||||
# 添加自动标签
|
||||
if self.config.auto_tags:
|
||||
tags.extend(self.config.default_tags)
|
||||
|
||||
# 添加来源标签
|
||||
if content.source:
|
||||
tags.append(content.source)
|
||||
|
||||
# 添加内容类型标签
|
||||
if content.content_type:
|
||||
tags.append(content.content_type.value)
|
||||
|
||||
# 添加 AI 生成的标签
|
||||
if content.tags:
|
||||
tags.extend(content.tags)
|
||||
|
||||
# 去重
|
||||
tags = list(dict.fromkeys(tags))
|
||||
|
||||
return tags
|
||||
|
||||
def _format_duration(self, duration_seconds: int) -> str:
|
||||
"""
|
||||
格式化时长为 HH:MM:SS
|
||||
|
||||
Args:
|
||||
duration_seconds: 时长(秒)
|
||||
|
||||
Returns:
|
||||
格式化的时长字符串
|
||||
"""
|
||||
if duration_seconds is None:
|
||||
return "N/A"
|
||||
|
||||
hours = duration_seconds // 3600
|
||||
minutes = (duration_seconds % 3600) // 60
|
||||
seconds = duration_seconds % 60
|
||||
|
||||
if hours > 0:
|
||||
return f"{hours}:{minutes:02d}:{seconds:02d}"
|
||||
else:
|
||||
return f"{minutes}:{seconds:02d}"
|
||||
|
||||
def _generate_body(self, content: Content) -> str:
|
||||
"""
|
||||
生成 Markdown 正文内容
|
||||
|
||||
Args:
|
||||
content: Content 对象
|
||||
|
||||
Returns:
|
||||
正文内容字符串
|
||||
"""
|
||||
body_parts = []
|
||||
|
||||
# 标题
|
||||
body_parts.append(f"# {content.title}")
|
||||
body_parts.append("")
|
||||
|
||||
# 一句话摘要
|
||||
if content.one_liner:
|
||||
body_parts.append(f"> {content.one_liner}")
|
||||
body_parts.append("")
|
||||
|
||||
# 摘要
|
||||
if content.summary:
|
||||
body_parts.append("## 摘要")
|
||||
body_parts.append("")
|
||||
body_parts.append(content.summary)
|
||||
body_parts.append("")
|
||||
|
||||
# 关键要点
|
||||
if content.key_points:
|
||||
body_parts.append("## 关键要点")
|
||||
body_parts.append("")
|
||||
for point in content.key_points:
|
||||
body_parts.append(f"- {point}")
|
||||
body_parts.append("")
|
||||
|
||||
# 大纲
|
||||
if content.outline:
|
||||
body_parts.append("## 大纲")
|
||||
body_parts.append("")
|
||||
for i, item in enumerate(content.outline, 1):
|
||||
body_parts.append(f"{i}. {item}")
|
||||
body_parts.append("")
|
||||
|
||||
# 时间线
|
||||
if content.timeline:
|
||||
body_parts.append("## 时间线")
|
||||
body_parts.append("")
|
||||
for entry in content.timeline:
|
||||
timestamp = entry.timestamp or "N/A"
|
||||
title = entry.title or ""
|
||||
description = entry.description or ""
|
||||
body_parts.append(f"### {timestamp} - {title}")
|
||||
if description:
|
||||
body_parts.append(description)
|
||||
body_parts.append("")
|
||||
|
||||
# 内容分析
|
||||
if content.analysis and content.analysis.outline:
|
||||
body_parts.append("## 详细分析")
|
||||
body_parts.append("")
|
||||
|
||||
analysis = content.analysis
|
||||
|
||||
# 前置知识
|
||||
if analysis.prerequisites:
|
||||
body_parts.append("### 前置知识")
|
||||
body_parts.append("")
|
||||
for prereq in analysis.prerequisites:
|
||||
body_parts.append(f"- {prereq}")
|
||||
body_parts.append("")
|
||||
|
||||
# 详细大纲
|
||||
if analysis.outline:
|
||||
body_parts.append("### 详细大纲")
|
||||
body_parts.append("")
|
||||
for section in analysis.outline:
|
||||
if isinstance(section, dict):
|
||||
title = section.get('title', '')
|
||||
content_text = section.get('content', '')
|
||||
body_parts.append(f"#### {title}")
|
||||
if content_text:
|
||||
body_parts.append(content_text)
|
||||
else:
|
||||
body_parts.append(str(section))
|
||||
body_parts.append("")
|
||||
|
||||
# 常见坑点
|
||||
if analysis.pitfalls:
|
||||
body_parts.append("### 常见坑点")
|
||||
body_parts.append("")
|
||||
for pitfall in analysis.pitfalls:
|
||||
body_parts.append(f"- {pitfall}")
|
||||
body_parts.append("")
|
||||
|
||||
# 实践建议
|
||||
if analysis.practice_advice:
|
||||
body_parts.append("### 实践建议")
|
||||
body_parts.append("")
|
||||
for advice in analysis.practice_advice:
|
||||
body_parts.append(f"- {advice}")
|
||||
body_parts.append("")
|
||||
|
||||
# 原始内容(如果有)
|
||||
if content.transcript:
|
||||
body_parts.append("## 原始内容")
|
||||
body_parts.append("")
|
||||
body_parts.append(content.transcript)
|
||||
body_parts.append("")
|
||||
|
||||
# 无字幕提示
|
||||
if not content.transcript:
|
||||
body_parts.append("*此视频无字幕,仅保存元数据*")
|
||||
body_parts.append("")
|
||||
|
||||
# 元数据
|
||||
body_parts.append("---")
|
||||
body_parts.append(f"- **来源**: [{content.author} - {content.title}]({content.url})")
|
||||
if content.duration:
|
||||
duration_str = self._format_duration(content.duration)
|
||||
body_parts.append(f"- **时长**: {duration_str}")
|
||||
body_parts.append(f"- **处理日期**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
return "\n".join(body_parts)
|
||||
Reference in New Issue
Block a user