feat(twitter_threads): full-thread extraction (conversations) + front-matter from target + corpus dedupe
fetch_thread_from_url now passes -o conversations=true (full threads, not single tweets), sorts posts chronologically, and sets root_post_id to the URL's target tweet. render_markdown builds front-matter from the target (root_post_id) post, not the earliest conversation tweet (fixes wrong @handle). Added dedupe_corpus.py: keeps deepest thread per conversation, deletes duplicates, keeps+marks unique branches (branch_of) + writes threads_index.json. 33 tests pass.
This commit is contained in:
@@ -207,7 +207,7 @@ def _posts_from_gallery_dl(data: Any) -> list[PostData]:
|
||||
|
||||
def fetch_thread_from_url(url: str, cookies_path: Path | None = None) -> Result[ThreadData]:
|
||||
clean = _normalize_url(url)
|
||||
args = ["gallery-dl", "--dump-json", "-o", "text-tweets=true"]
|
||||
args = ["gallery-dl", "--dump-json", "-o", "text-tweets=true", "-o", "conversations=true"]
|
||||
if cookies_path is not None:
|
||||
args += ["--cookies", str(cookies_path)]
|
||||
args.append(clean)
|
||||
@@ -224,7 +224,10 @@ def fetch_thread_from_url(url: str, cookies_path: Path | None = None) -> Result[
|
||||
posts = _posts_from_gallery_dl(data)
|
||||
if not posts:
|
||||
return Result.err(make_error("ParseError", "fetch_thread_from_url", "no tweet metadata in gallery-dl output"))
|
||||
return Result.ok(ThreadData(root_post_id=posts[0].post_id, posts=tuple(posts), source_url=clean))
|
||||
posts.sort(key=lambda p: int(p.post_id) if p.post_id.isdigit() else 0)
|
||||
target_id = clean.rstrip("/").split("/")[-1]
|
||||
root_id = target_id if target_id.isdigit() else posts[0].post_id
|
||||
return Result.ok(ThreadData(root_post_id=root_id, posts=tuple(posts), source_url=clean))
|
||||
|
||||
|
||||
def thread_to_dict(thread: ThreadData) -> dict[str, Any]:
|
||||
|
||||
@@ -19,8 +19,14 @@ def _format_view_count(value: int | None) -> str:
|
||||
return "null"
|
||||
return str(value)
|
||||
|
||||
def _root_post(thread: ThreadData) -> PostData:
|
||||
for p in thread.posts:
|
||||
if p.post_id == thread.root_post_id:
|
||||
return p
|
||||
return thread.posts[0]
|
||||
|
||||
def _render_yaml(thread: ThreadData, title: str) -> list[str]:
|
||||
root = thread.posts[0]
|
||||
root = _root_post(thread)
|
||||
return [
|
||||
"---",
|
||||
f'title: "{title}"',
|
||||
@@ -59,7 +65,7 @@ def _render_post(post: PostData, n: int, id_to_index: dict[str, int], media_name
|
||||
def render_markdown(thread: ThreadData, media_names: dict[str, list[str]], output: Path) -> Result[Path]:
|
||||
lines: list[str] = []
|
||||
id_to_index = _build_id_to_index(thread)
|
||||
root = thread.posts[0]
|
||||
root = _root_post(thread)
|
||||
first_line = root.text.splitlines()[0] if root.text.splitlines() else ""
|
||||
title = first_line[:80]
|
||||
lines.extend(_render_yaml(thread, title))
|
||||
|
||||
Reference in New Issue
Block a user