Files
2026-07-13 12:29:17 +08:00

146 lines
4.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""发完视频后跑一次:抓评论/数据 → 生成 NotebookLM 友好的 md。
用法:
python review.py # 交互式选视频
python review.py login # 仅登录(首次)
python review.py video <aweme_id> [script.txt] # 直接指定视频
"""
from __future__ import annotations
import asyncio
import sys
from pathlib import Path
import crawler
import renderer
from paths import videos_dir
def _prompt(msg: str) -> str:
try:
return input(msg).strip()
except EOFError:
return ""
def _pick_video(videos: list[dict]) -> dict | None:
if not videos:
print("未抓到视频列表。请确认创作者中心已登录,或页面结构已变,需要更新 crawler。")
return None
print("\n最近视频:")
for i, v in enumerate(videos):
t = renderer._fmt_time(v.get("create_time", 0))
desc = (v.get("desc") or "").replace("\n", " ")[:40]
print(f" [{i}] {t} | 播放 {renderer._fmt_num(v.get('play_count'))} | {desc}")
choice = _prompt("\n选择序号(回车取消):")
if not choice.isdigit():
return None
idx = int(choice)
if 0 <= idx < len(videos):
return videos[idx]
return None
def _resolve_script(raw: str) -> str:
"""允许直接拖拽文件到终端;兼容 macOS 转义空格。"""
p = raw.strip().strip("'").strip('"').replace("\\ ", " ")
if not p:
return ""
path = Path(p).expanduser()
if path.is_file():
return path.read_text(encoding="utf-8", errors="ignore")
print(f"[警告] 找不到文件 {path},稿子留空。")
return ""
async def run() -> None:
"""交互式:先列最近 10 条,用户选一个,再让用户拖稿子,最后抓取。
注意:会打开两次 Chromium(一次选视频,一次抓全量)。
"""
active_videos_dir = videos_dir()
active_videos_dir.mkdir(parents=True, exist_ok=True)
print("[选视频] 打开创作者中心拉列表……")
sess = await crawler.Session.open()
try:
videos = await crawler.fetch_recent_videos(sess, limit=10)
finally:
await sess.close()
video = _pick_video(videos)
if not video:
print("已取消。")
return
script_raw = _prompt("把稿子 txt 拖进来(或回车跳过):")
script_path: str | None = None
if script_raw.strip():
p = Path(script_raw.strip().strip("'").strip('"').replace("\\ ", " ")).expanduser()
if p.is_file():
script_path = str(p)
else:
print(f"[警告] 找不到 {p},稿子留空。")
await run_with_id(video["aweme_id"], script_path)
async def run_with_id(aweme_id: str, script_path: str | None) -> None:
active_videos_dir = videos_dir()
active_videos_dir.mkdir(parents=True, exist_ok=True)
script = ""
if script_path:
p = Path(script_path).expanduser()
if p.is_file():
script = p.read_text(encoding="utf-8", errors="ignore")
print(f"稿子:{p.name}{len(script)} 字符)")
else:
print(f"[警告] 找不到稿子 {p}")
print(f"[抓取] 视频 {aweme_id}")
result = await crawler.fetch_all(aweme_id)
video = result["video"]
detail = result["detail"]
comments = result["comments"]
out_dir = renderer.output_dir_for(video, active_videos_dir)
out_dir.mkdir(parents=True, exist_ok=True)
if script:
(out_dir / "script.txt").write_text(script, encoding="utf-8")
md = renderer.render_report(video, script, comments, detail.get("captured"))
report = out_dir / "report.md"
report.write_text(md, encoding="utf-8")
print(f"\n{report}")
def main() -> None:
if len(sys.argv) > 1 and sys.argv[1] == "login":
asyncio.run(crawler.ensure_login())
return
if len(sys.argv) > 1 and sys.argv[1] == "video":
if len(sys.argv) < 3:
print("用法:python review.py video <aweme_id> [script.txt]")
sys.exit(2)
aweme_id = sys.argv[2]
script_path = sys.argv[3] if len(sys.argv) > 3 else None
asyncio.run(run_with_id(aweme_id, script_path))
return
if len(sys.argv) > 1 and sys.argv[1] == "list":
async def _list() -> None:
sess = await crawler.Session.open()
try:
videos = await crawler.fetch_recent_videos(sess, limit=20)
finally:
await sess.close()
for i, v in enumerate(videos):
t = renderer._fmt_time(v.get("create_time", 0))
desc = (v.get("desc") or "").replace("\n", " ")[:50]
print(f"[{i}] {v['aweme_id']} {t} 播放{renderer._fmt_num(v.get('play_count'))} {desc}")
asyncio.run(_list())
return
asyncio.run(run())
if __name__ == "__main__":
main()