mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
107 lines
3.8 KiB
Python
107 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Update an existing Substack post from a prepared local Markdown bundle."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
from dotenv import load_dotenv
|
|
from substack import Api
|
|
from substack.post import Post
|
|
|
|
|
|
def markdown_body_without_title(markdown: str) -> tuple[str, str, str]:
|
|
lines = markdown.splitlines()
|
|
title = ""
|
|
subtitle = ""
|
|
body_start = 0
|
|
if lines and lines[0].startswith("# "):
|
|
title = lines[0][2:].strip()
|
|
body_start = 1
|
|
if len(lines) > 1 and lines[1].startswith("## "):
|
|
subtitle = lines[1][3:].strip()
|
|
body_start = 2
|
|
while body_start < len(lines) and not lines[body_start].strip():
|
|
body_start += 1
|
|
return title, subtitle, "\n".join(lines[body_start:]).strip() + "\n"
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Update an existing Substack post from local Markdown.")
|
|
parser.add_argument("markdown_path", type=Path)
|
|
parser.add_argument("--post-id", type=int, required=True)
|
|
parser.add_argument("--env-path", type=Path, default=Path.home() / ".substack.env")
|
|
parser.add_argument("--publication-url", default="https://froginnponds.substack.com")
|
|
parser.add_argument("--publish", action="store_true", help="Publish the updated draft without sending email.")
|
|
args = parser.parse_args()
|
|
|
|
load_dotenv(args.env_path)
|
|
cookies = os.getenv("COOKIES_STRING")
|
|
if not cookies:
|
|
raise SystemExit("COOKIES_STRING is missing from the Substack env file")
|
|
|
|
markdown_path = args.markdown_path.resolve()
|
|
markdown = markdown_path.read_text(encoding="utf-8")
|
|
title, subtitle, body_markdown = markdown_body_without_title(markdown)
|
|
if not title:
|
|
raise SystemExit("Markdown must start with a # title")
|
|
|
|
api = Api(cookies_string=cookies, publication_url=args.publication_url)
|
|
user_id = api.get_user_id()
|
|
existing = api.get_draft(args.post_id)
|
|
|
|
backup_dir = markdown_path.parent / "substack_backups"
|
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
backup_path = backup_dir / f"post_{args.post_id}_{stamp}.json"
|
|
backup_path.write_text(json.dumps(existing, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
|
|
cwd = Path.cwd()
|
|
try:
|
|
os.chdir(markdown_path.parent)
|
|
post = Post(
|
|
title=title,
|
|
subtitle=subtitle,
|
|
user_id=user_id,
|
|
audience=existing.get("audience") or "everyone",
|
|
write_comment_permissions=existing.get("write_comment_permissions") or "everyone",
|
|
)
|
|
post.from_markdown(body_markdown, api=api)
|
|
finally:
|
|
os.chdir(cwd)
|
|
|
|
payload = post.get_draft()
|
|
payload["draft_bylines"] = existing.get("draft_bylines") or payload.get("draft_bylines")
|
|
payload["draft_section_id"] = existing.get("draft_section_id")
|
|
payload["section_chosen"] = existing.get("section_chosen", True)
|
|
|
|
updated = api.put_draft(args.post_id, **payload)
|
|
print(json.dumps({
|
|
"updated_post_id": args.post_id,
|
|
"title": title,
|
|
"subtitle": subtitle,
|
|
"backup_path": str(backup_path),
|
|
"draft_updated_at": updated.get("draft_updated_at"),
|
|
"is_published": updated.get("is_published"),
|
|
"published": False,
|
|
}, indent=2))
|
|
|
|
if args.publish:
|
|
api.prepublish_draft(args.post_id)
|
|
published = api.publish_draft(args.post_id, send=False, share_automatically=False)
|
|
print(json.dumps({
|
|
"published": True,
|
|
"post_id": args.post_id,
|
|
"slug": published.get("slug"),
|
|
"canonical_url": published.get("canonical_url") or published.get("canonicalUrl"),
|
|
}, indent=2))
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|