386 lines
11 KiB
Python
386 lines
11 KiB
Python
"""
|
|
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)
|