""" 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)