You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Major release restructuring the project from single-file script to
installable package, shipping paywall detection, concurrency, resume,
logging, CI, and a 58-test suite.
Breaking changes:
- Flat substack2md.py removed; invoke via `substack2md` console script
or `python -m substack2md` after `pip install .`.
- requirements.txt and tests/requirements-dev.txt removed; use
pyproject.toml (`pip install -e ".[dev]"` for dev).
CHANGELOG.md carries the full release notes and a migration table.
README: expanded the launch-browser.sh section with what it does,
security properties (isolated profile, loopback-only port), and a
pointer for non-macOS users; frontmatter example bumped to v2.0.0.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+48-17Lines changed: 48 additions & 17 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,31 +6,62 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
7
7
## [Unreleased]
8
8
9
-
### Added
10
-
- CI: GitHub Actions workflow running `pytest tests/` on push and pull request across Python 3.10, 3.11, 3.12, 3.13.
11
-
-`CONTRIBUTING.md` with local dev setup, PR guidelines, and test instructions.
12
-
-`CHANGELOG.md` (this file).
9
+
_Nothing yet._
13
10
14
-
### Changed
15
-
- Centralized the version string into a single `__version__` module constant. All `source:` frontmatter lines now derive from it instead of three hardcoded copies.
16
-
- README: documented all four known Substack `audience` enum values (`everyone`, `only_free`, `only_paid`, `founding`) and the "unknown tier -> `is_paid=null`" contract.
- Replaced deprecated `datetime.utcnow()` with `datetime.now(timezone.utc)` so Python 3.12+ runs without `DeprecationWarning` and Python 3.14 (which removes `utcnow`) still works.
13
+
Major release. Restructures the project from a single-file script into an installable package with a console entry point, ships a much larger feature set, and tightens every user-facing surface. All Python-level library imports (`import substack2md; substack2md.fetch_paywall_status(...)`) remain backward compatible.
14
+
15
+
### Breaking changes
16
+
17
+
-**Removed the flat `substack2md.py` file.** Invocation has moved from `python substack2md.py URL` to the installed console script `substack2md URL` (or `python -m substack2md URL`). After `pip install .` the CLI is on your PATH.
18
+
-**Removed `requirements.txt` and `tests/requirements-dev.txt`.**`pyproject.toml` is now the single source of truth for dependencies. Use `pip install .` or `pip install -e ".[dev]"` instead.
-`--detect-paywall` CLI flag (from #1, @drewid74): queries Substack's public `/api/v1/posts/{slug}` API to classify posts as free or subscriber-only. Adds `is_paid` (bool) and `audience` (str) to YAML frontmatter. Opt-in, graceful fallback to `null` on API errors, no additional authentication required.
26
-
- Test suite: 30 tests under `tests/` covering audience decoding, HTTP failure modes, request shape, frontmatter serialization, CLI wiring, and publication-slug edge cases. Opt-in live smoke test under `SUBSTACK2MD_LIVE=1`.
27
-
-`tests/EVALS.md` documenting the paywall-detection eval report.
30
+
31
+
-**`--detect-paywall` flag** (originally #1 from @drewid74): queries Substack's public `/api/v1/posts/{slug}` API to classify posts as free or subscriber-only. Writes `is_paid` and `audience` fields to YAML frontmatter. Opt-in, graceful fallback to `null` on API errors, no additional authentication required.
32
+
-**`--concurrency N` flag**: opt-in parallel processing. Defaults to 1 (sequential). Posts from the same publication are still serialized via per-host locks to avoid bot heuristics; parallelism is across different publications only.
33
+
-**Resume-from-interrupt**: every successfully written URL is appended to `<base-dir>/.substack2md-state`. Subsequent runs skip already-completed URLs before any network call. Pass `--no-resume` to disable. Clean `KeyboardInterrupt` handling reports progress before exit.
34
+
-**`--log-level {DEBUG,INFO,WARNING,ERROR}`, `--quiet`/`-q`, `--version` flags**. Diagnostics now flow through the `logging` module; `[ok]`/`[skip]` progress becomes `INFO`-level so `--quiet` can suppress them cleanly.
35
+
-**Teaser-warning detection**: when `--detect-paywall` reports a paid post and the extracted body is under 300 words, substack2md logs a warning that you may have only captured the teaser and need to authenticate in the CDP-connected browser.
36
+
-**Custom-domain Substack support**: publications with custom domains (e.g. stratechery.com) now route paywall API calls to their canonical `<pub>.substack.com` subdomain via the new `resolve_substack_canonical()` helper.
37
+
-**Richer tag extraction**: merges `<meta name="keywords">`, ld+json `keywords`, and ld+json `articleSection` before normalization, so posts get the author's real taxonomy instead of just `["substack"]`.
38
+
-**`--from-md` + `--detect-paywall`**: backfill paywall metadata on existing markdown archives without re-fetching HTML.
39
+
-**`launch-browser.sh`**: macOS helper that detects Brave or Chrome, isolates a dedicated CDP profile at `$HOME/.*-cdp-profile`, opens port 9222 on loopback, and verifies the endpoint before exiting.
40
+
-**CI via GitHub Actions**: `pytest` and `ruff` run on push and PR across Python 3.10, 3.11, 3.12, 3.13.
-**`pyproject.toml`**: registers `substack2md` as a console script, declares runtime and dev dependencies, sets Python ≥ 3.10, wires the `ruff` lint/format config and the `pytest` config.
48
+
-**README**: rewritten installation and Quick Start for the package flow; full and current CLI reference; paywall section documents all four `audience` enum values; links to `launch-browser.sh` and `CONTRIBUTING.md`.
49
+
-**Logging**: `print(..., file=sys.stderr)` replaced with the `substack2md` logger. Formatter includes level + logger name so downstream aggregators can filter.
50
+
-**Code style**: full `ruff` format pass applied. 100-char soft limit, isort-sorted imports, modern type-hint syntax under `UP`.
28
51
29
52
### Fixed
30
-
- Founding-tier posts (Substack `audience: founding`) are now correctly classified as `is_paid: true`. Previously matched only `only_paid` exactly, silently leaking paid content as free.
31
-
- Missing `audience` field in a 200 response now returns `(is_paid=None, audience=None)` instead of defaulting to `"everyone"/False`. Matches the docstring's null-on-uncertainty promise.
32
-
- Unknown audience values (future Substack tiers) are preserved verbatim as `audience` but `is_paid` is left as `null` so downstream workflows treat the post as "status unknown" rather than silently free.
53
+
54
+
-**Founding-tier posts** (`audience: founding`) are now correctly classified as `is_paid: true`. Previously matched only `only_paid` exactly, which silently leaked paid content as free.
55
+
-**Missing `audience` field** in a 200 response now returns `(is_paid=None, audience=None)` instead of defaulting to `"everyone"`/`False`. Matches the documented null-on-uncertainty contract.
56
+
-**Unknown audience values** (future Substack tiers) are preserved verbatim as `audience` but `is_paid` is left as `null` so downstream treats the post as "status unknown" rather than silently free.
57
+
-**`datetime.utcnow()` deprecation**: swapped for `datetime.now(timezone.utc)` so Python 3.12+ runs without `DeprecationWarning` and Python 3.14 (which removes `utcnow`) works.
58
+
-**CDP target leak**: `CDPClient.fetch_html` now wraps navigate/eval in `try/finally` so `Target.closeTarget` always runs, preventing tab-pool exhaustion during long batches.
59
+
-**`--timeout` not threaded through to paywall API**: the CLI `--timeout` value now reaches `fetch_paywall_status` instead of being hardcoded to 10s.
33
60
34
61
## [1.1.0] - prior
35
62
36
63
First tagged reference point. CDP-driven Substack-to-markdown converter with Obsidian wikilink rewriting, publication mapping, transcript cleanup, and batch URL file support.
Copy file name to clipboardExpand all lines: README.md
+11-1Lines changed: 11 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -47,6 +47,16 @@ The repo ships a helper that detects Brave or Chrome, isolates a dedicated CDP p
47
47
./launch-browser.sh
48
48
```
49
49
50
+
What it does:
51
+
52
+
- Prefers Brave; falls back to Chrome (arch-aware on Apple Silicon).
53
+
- Creates an isolated browser profile at `$HOME/.brave-cdp-profile` or `$HOME/.chrome-cdp-profile` so your main browsing session, cookies, and extensions are untouched.
54
+
- Binds `--remote-debugging-port=9222` to loopback only (`127.0.0.1`) and sets `--remote-allow-origins` so only local clients can connect.
55
+
- If port 9222 is already in use, prompts before killing the existing process.
56
+
- Verifies CDP is reachable after launch.
57
+
58
+
The script is macOS-only (uses `open` and `/Applications`). Linux or Windows users can use the manual invocations below, or submit a PR adding platform support.
59
+
50
60
Prefer to run the commands yourself? The underlying invocations are:
0 commit comments