diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 60842ac..2efe480 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -60,6 +60,8 @@ jobs: - name: Build Quartz working-directory: site run: npx quartz build -d .. + - name: Patch sidebar title link + run: python ../publish/quartz-patches/patch-title-link.py - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: diff --git a/publish/quartz-patches/patch-title-link.py b/publish/quartz-patches/patch-title-link.py new file mode 100644 index 0000000..cef9e25 --- /dev/null +++ b/publish/quartz-patches/patch-title-link.py @@ -0,0 +1,65 @@ +"""Patch all built HTML files so the sidebar title link goes to /Threadwell/. + +The `

` link is generated by the +`@quartz-community/page-title` plugin from a relative href that breaks +when the URL bar lacks a trailing slash. We rewrite the href to the +absolute site base path so it never lands on the bare domain. + +Idempotent. Safe to re-run. +""" +from __future__ import annotations + +import re +import sys +from pathlib import Path + +SCRIPT = Path(__file__).resolve() +VAULT = SCRIPT.parents[2] + +# Detect base path from the publish overlay if present, else default to /Threadwell/ +overlay = VAULT / "publish" / "quartz.config.yaml" +site_base = "/Threadwell/" +if overlay.is_file(): + for line in overlay.read_text(encoding="utf-8").splitlines(): + if line.startswith("baseUrl:"): + host = line.split(":", 1)[1].strip() + if "/" in host: + site_base = "/" + host.split("/", 1)[1].rstrip("/") + "/" + else: + site_base = "/" + break + +PUBLIC = VAULT / "site" / "public" + +PATTERN = re.compile( + r'()' +) +REPLACEMENT = rf"\1{site_base}\2" + + +def patch_html(path: Path) -> bool: + text = path.read_text(encoding="utf-8", errors="ignore") + new = PATTERN.sub(REPLACEMENT, text) + if new != text: + path.write_text(new, encoding="utf-8") + return True + return False + + +def main() -> int: + if not PUBLIC.is_dir(): + print(f"missing {PUBLIC}") + return 1 + changed = 0 + total = 0 + for path in PUBLIC.rglob("*.html"): + total += 1 + if patch_html(path): + changed += 1 + print(f"site base: {site_base}") + print(f"total {total}, changed {changed}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/twitter/lift_catbox.py b/scripts/twitter/lift_catbox.py index a8edb75..f396d15 100644 --- a/scripts/twitter/lift_catbox.py +++ b/scripts/twitter/lift_catbox.py @@ -9,6 +9,9 @@ Default without --orig: only rows already publish=true. After upload, any note token that is the local filename (backticked or bare) or a previous url for that row becomes ![](https://files.catbox.moe/…). + +Per-upload curl: --max-time 120, --retry 3, --retry-delay 2, --retry-all-errors. +Errors are sanitized to redact the userhash before printing. """ from __future__ import annotations @@ -17,12 +20,28 @@ import json import re import subprocess import sys +import time import tomllib from pathlib import Path from paths import VAULT as VAULT_ROOT API = "https://catbox.moe/user/api.php" +MAX_TIME = 120 +RETRIES = 3 +RETRY_DELAY = 5 +THROTTLE_SECS = 5.0 +LIMIT_RATE = "500k" + + +def _redact_cmd(cmd: list[str]) -> str: + out: list[str] = [] + for tok in cmd: + if tok.startswith("userhash="): + out.append("userhash=") + else: + out.append(tok) + return " ".join(out) def load_userhash(vault: Path) -> str: @@ -54,6 +73,15 @@ def upload(path: Path, userhash: str) -> str: cmd = [ "curl.exe", "-sS", + "--max-time", + str(MAX_TIME), + "--retry", + str(RETRIES), + "--retry-delay", + str(RETRY_DELAY), + "--retry-all-errors", + "--limit-rate", + LIMIT_RATE, "-F", "reqtype=fileupload", "-F", @@ -62,7 +90,13 @@ def upload(path: Path, userhash: str) -> str: f"fileToUpload=@{path}", API, ] - raw = subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT) + try: + raw = subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as exc: + raise SystemExit( + f"catbox upload failed for {path.name} " + f"(curl exit {exc.returncode}): {_redact_cmd(cmd)}" + ) from None url = raw.strip() if not url.startswith("https://"): raise SystemExit(f"catbox refused {path.name}: {url[:200]}") @@ -122,9 +156,10 @@ def main(argv: list[str] | None = None) -> int: userhash = None if args.dry_run else load_userhash(args.vault) uploaded = 0 - for item in items: - if not item.get("publish"): - continue + publishable = [it for it in items if it.get("publish")] + for i, item in enumerate(publishable): + if i > 0 and not args.dry_run: + time.sleep(THROTTLE_SECS) name = str(item.get("filename") or "") src = args.thread / name if not src.is_file():