08f7c9f26c
- Add extractors layer with BilibiliExtractor (Playwright stealth mode) - Add outputs layer with ObsidianWriter and GitSync - Implement subtitle/no-subtitle graceful degradation - Update config with output and bilibili extraction settings - Complete integration pipeline: extract → process → output → sync - Add comprehensive tests (100% pass rate) - Update documentation (SKILL.md, README.md, requirements.txt)
318 lines
10 KiB
Python
318 lines
10 KiB
Python
"""
|
||
测试输出器功能
|
||
"""
|
||
import sys
|
||
import os
|
||
from pathlib import Path
|
||
|
||
# 添加项目路径到 sys.path
|
||
project_root = Path(__file__).parent
|
||
sys.path.insert(0, str(project_root))
|
||
|
||
from src.outputs import ObsidianWriter, GitSync
|
||
from src.models import Content, ContentType, ContentStatus, TimelineEntry, ContentAnalysis
|
||
from src.config import Config
|
||
|
||
|
||
def create_test_content() -> Content:
|
||
"""创建测试数据"""
|
||
content = Content(
|
||
id="test001",
|
||
source="bilibili",
|
||
source_id="BV1234567890",
|
||
url="https://www.bilibili.com/video/BV1234567890",
|
||
title="Python 高级编程教程 - 装饰器深入解析",
|
||
author="编程大师",
|
||
description="深入讲解 Python 装饰器的原理和应用",
|
||
duration=1800, # 30分钟
|
||
content_type=ContentType.VIDEO,
|
||
status=ContentStatus.GENERATED,
|
||
transcript="""欢迎来到本期的 Python 高级编程教程。
|
||
|
||
今天我们要深入探讨装饰器的原理和应用。
|
||
|
||
首先,让我们理解装饰器的基本概念...
|
||
|
||
[更多字幕内容...]""",
|
||
summary="本教程深入讲解了 Python 装饰器的原理,包括闭包、函数装饰器、类装饰器等核心概念,并通过实际案例展示了装饰器在日志、缓存、权限验证等场景中的应用。",
|
||
one_liner="30分钟深入理解 Python 装饰器,从原理到实战",
|
||
key_points=[
|
||
"装饰器本质上是高阶函数",
|
||
"闭包是装饰器实现的基础",
|
||
"@语法糖只是语法糖",
|
||
"装饰器可以带参数",
|
||
"类装饰器也是常见模式",
|
||
],
|
||
tags=["Python", "装饰器", "高级编程", "教程"],
|
||
quality_score=85.5,
|
||
outline=[
|
||
"什么是装饰器",
|
||
"装饰器的原理",
|
||
"函数装饰器",
|
||
"带参数的装饰器",
|
||
"类装饰器",
|
||
"装饰器应用场景",
|
||
"实战案例",
|
||
],
|
||
timeline=[
|
||
TimelineEntry(
|
||
timestamp="00:00",
|
||
title="课程介绍",
|
||
description="介绍本课程的主要内容和学习目标"
|
||
),
|
||
TimelineEntry(
|
||
timestamp="02:30",
|
||
title="装饰器基础",
|
||
description="讲解装饰器的基本概念和语法"
|
||
),
|
||
TimelineEntry(
|
||
timestamp="10:00",
|
||
title="装饰器原理",
|
||
description="深入理解装饰器背后的实现原理"
|
||
),
|
||
TimelineEntry(
|
||
timestamp="20:00",
|
||
title="实战案例",
|
||
description="通过实际代码演示装饰器的应用"
|
||
),
|
||
],
|
||
category="编程",
|
||
)
|
||
|
||
# 添加详细分析
|
||
content.analysis = ContentAnalysis(
|
||
one_liner="30分钟深入理解 Python 装饰器,从原理到实战",
|
||
summary="本教程深入讲解了 Python 装饰器的原理,包括闭包、函数装饰器、类装饰器等核心概念。",
|
||
key_points=[
|
||
"装饰器本质上是高阶函数",
|
||
"闭包是装饰器实现的基础",
|
||
"@语法糖只是语法糖",
|
||
],
|
||
tags=["Python", "装饰器"],
|
||
quality_score=85.5,
|
||
prerequisites=["Python 基础语法", "函数基础", "面向对象基础"],
|
||
outline=[
|
||
{"title": "装饰器基础", "content": "什么是装饰器,基本语法"},
|
||
{"title": "实现原理", "content": "闭包的作用,装饰器的执行流程"},
|
||
{"title": "高级用法", "content": "带参数的装饰器,类装饰器"},
|
||
{"title": "实际应用", "content": "日志记录、性能测试、权限验证"},
|
||
],
|
||
pitfalls=[
|
||
"忘记保留原函数的元数据(如 __name__, __doc__)",
|
||
"装饰器中修改参数可能导致意外行为",
|
||
"多层装饰器的执行顺序容易混淆",
|
||
],
|
||
practice_advice=[
|
||
"多动手实践,编写不同类型的装饰器",
|
||
"使用 functools.wraps 保留函数元数据",
|
||
"在复杂场景下考虑使用类装饰器",
|
||
],
|
||
)
|
||
|
||
return content
|
||
|
||
|
||
def create_test_content_no_subtitle() -> Content:
|
||
"""创建无字幕的测试数据"""
|
||
content = Content(
|
||
id="test002",
|
||
source="bilibili",
|
||
source_id="BV0987654321",
|
||
url="https://www.bilibili.com/video/BV0987654321",
|
||
title="无字幕测试视频",
|
||
author="测试UP主",
|
||
description="这是一个没有字幕的测试视频",
|
||
duration=300, # 5分钟
|
||
content_type=ContentType.VIDEO,
|
||
status=ContentStatus.GENERATED,
|
||
transcript="", # 空字幕
|
||
summary="这是一个测试视频,用于验证无字幕时的输出",
|
||
one_liner="测试无字幕视频的输出功能",
|
||
key_points=[
|
||
"测试要点1",
|
||
"测试要点2",
|
||
],
|
||
tags=["测试"],
|
||
quality_score=60.0,
|
||
)
|
||
|
||
return content
|
||
|
||
|
||
def test_obsidian_writer():
|
||
"""测试 ObsidianWriter"""
|
||
print("=" * 60)
|
||
print("测试 ObsidianWriter")
|
||
print("=" * 60)
|
||
|
||
# 创建测试配置
|
||
config = Config()
|
||
|
||
# 使用临时测试目录
|
||
test_vault = Path("/tmp/test_obsidian_vault")
|
||
if test_vault.exists():
|
||
import shutil
|
||
shutil.rmtree(test_vault)
|
||
test_vault.mkdir(parents=True, exist_ok=True)
|
||
|
||
# 修改配置使用测试 vault
|
||
config._config['obsidian']['vault_path'] = str(test_vault)
|
||
|
||
# 创建写入器
|
||
writer = ObsidianWriter(config)
|
||
|
||
# 测试1: 有字幕的内容
|
||
print("\n测试1: 写入有字幕的内容")
|
||
content1 = create_test_content()
|
||
file_path1 = writer.write(content1, config)
|
||
|
||
if file_path1:
|
||
print(f"✓ 成功写入: {file_path1}")
|
||
# 检查文件是否存在
|
||
if Path(file_path1).exists():
|
||
print(f"✓ 文件存在")
|
||
# 读取并显示前20行
|
||
print("\n文件内容预览(前20行):")
|
||
with open(file_path1, 'r', encoding='utf-8') as f:
|
||
lines = f.readlines()[:20]
|
||
for i, line in enumerate(lines, 1):
|
||
print(f"{i:2d}: {line.rstrip()}")
|
||
else:
|
||
print(f"✗ 文件不存在")
|
||
else:
|
||
print("✗ 写入失败")
|
||
|
||
# 测试2: 无字幕的内容
|
||
print("\n" + "-" * 60)
|
||
print("测试2: 写入无字幕的内容")
|
||
content2 = create_test_content_no_subtitle()
|
||
file_path2 = writer.write(content2, config)
|
||
|
||
if file_path2:
|
||
print(f"✓ 成功写入: {file_path2}")
|
||
# 检查文件是否存在
|
||
if Path(file_path2).exists():
|
||
print(f"✓ 文件存在")
|
||
# 读取并显示最后几行
|
||
print("\n文件内容预览(最后10行):")
|
||
with open(file_path2, 'r', encoding='utf-8') as f:
|
||
lines = f.readlines()
|
||
for line in lines[-10:]:
|
||
print(f" {line.rstrip()}")
|
||
else:
|
||
print(f"✗ 文件不存在")
|
||
else:
|
||
print("✗ 写入失败")
|
||
|
||
# 检查目录结构
|
||
print("\n" + "-" * 60)
|
||
print("目录结构:")
|
||
print(f"{test_vault}/")
|
||
for root, dirs, files in os.walk(test_vault):
|
||
level = root.replace(str(test_vault), '').count(os.sep)
|
||
indent = ' ' * 2 * level
|
||
print(f"{indent}{os.path.basename(root)}/")
|
||
subindent = ' ' * 2 * (level + 1)
|
||
for file in files:
|
||
print(f"{subindent}{file}")
|
||
|
||
# 清理测试目录
|
||
print("\n" + "-" * 60)
|
||
print(f"测试目录: {test_vault}")
|
||
print("测试完成后可手动删除测试目录")
|
||
|
||
return file_path1 is not None and file_path2 is not None
|
||
|
||
|
||
def test_git_sync():
|
||
"""测试 GitSync"""
|
||
print("\n" + "=" * 60)
|
||
print("测试 GitSync")
|
||
print("=" * 60)
|
||
|
||
# 创建测试 vault
|
||
test_vault = Path("/tmp/test_git_vault")
|
||
if test_vault.exists():
|
||
import shutil
|
||
shutil.rmtree(test_vault)
|
||
test_vault.mkdir(parents=True, exist_ok=True)
|
||
|
||
# 初始化 Git 仓库
|
||
try:
|
||
subprocess_result = os.system(f"cd {test_vault} && git init && git config user.email 'test@example.com' && git config user.name 'Test User' > /dev/null 2>&1")
|
||
print(f"✓ Git 仓库初始化成功")
|
||
except:
|
||
print("✗ Git 初始化失败,跳过 GitSync 测试")
|
||
return False
|
||
|
||
# 创建测试配置
|
||
config = Config()
|
||
config._config['obsidian']['vault_path'] = str(test_vault)
|
||
config._config['git']['enabled'] = True
|
||
config._config['git']['auto_push'] = False # 不测试 push,避免需要远程仓库
|
||
|
||
# 创建 Git 同步器
|
||
sync = GitSync(config)
|
||
|
||
# 测试1: 检查 Git 状态
|
||
print("\n测试1: 检查是否为 Git 仓库")
|
||
is_repo = sync._is_git_repo()
|
||
print(f"✓ 是否为 Git 仓库: {is_repo}")
|
||
|
||
# 测试2: 创建测试文件并同步
|
||
print("\n测试2: 创建文件并同步")
|
||
test_file = test_vault / "test.md"
|
||
test_file.write_text("# 测试文件\n\n这是一个测试文件。")
|
||
|
||
result = sync.sync(config)
|
||
print(f"✓ 同步结果: {result}")
|
||
|
||
# 测试3: 检查 Git 状态
|
||
print("\n测试3: 检查 Git 状态")
|
||
os.system(f"cd {test_vault} && git log --oneline -n 3")
|
||
|
||
print("\n" + "-" * 60)
|
||
print(f"测试目录: {test_vault}")
|
||
print("测试完成后可手动删除测试目录")
|
||
|
||
return result
|
||
|
||
|
||
def main():
|
||
"""主测试函数"""
|
||
print("\n" + "=" * 60)
|
||
print("Obsidian-KB 输出器测试")
|
||
print("=" * 60)
|
||
|
||
results = {}
|
||
|
||
# 测试 ObsidianWriter
|
||
results['obsidian_writer'] = test_obsidian_writer()
|
||
|
||
# 测试 GitSync
|
||
results['git_sync'] = test_git_sync()
|
||
|
||
# 汇总结果
|
||
print("\n" + "=" * 60)
|
||
print("测试结果汇总")
|
||
print("=" * 60)
|
||
for test_name, result in results.items():
|
||
status = "✓ 通过" if result else "✗ 失败"
|
||
print(f"{test_name}: {status}")
|
||
|
||
# 总体结果
|
||
all_passed = all(results.values())
|
||
print("\n" + "=" * 60)
|
||
if all_passed:
|
||
print("✓ 所有测试通过!")
|
||
else:
|
||
print("✗ 部分测试失败")
|
||
print("=" * 60)
|
||
|
||
return all_passed
|
||
|
||
|
||
if __name__ == "__main__":
|
||
success = main()
|
||
sys.exit(0 if success else 1)
|