ZMediumToMarkdown 3.7.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b9b990e8f990e996a5a7dd230950f572a2a6ee80cc508a98dc9dc2122c870e3
4
- data.tar.gz: 8580bbafe3ee759a0e5790c98c663388cae5e6a175a1570e34c1be6e20c73973
3
+ metadata.gz: 8b71926fd220e5524846da26d0ff13d8faf877a7320d40445c1e1561842fa125
4
+ data.tar.gz: 63910f5efe83d0ac58ff677374504b47455c0bb42b5d332273a40fa96fa687f1
5
5
  SHA512:
6
- metadata.gz: 84a68dd6a63a7cb94bf86e07fa4de30e6e50c4f364ca1dbb423e9cdd325d6c356b26dabcab8909cf84a8839fe96c04ac31eaceae6c8b74acbd2ea64893991339
7
- data.tar.gz: dcf05e3ade1a2653ea14ffb8f9c9881592287f45f1e13e0ff3d7db5f483cc62b4da56413171c494a44a15928f4c974857aa54dffe26c961206b3ed2538105e87
6
+ metadata.gz: 3f6c474eebc28e5fef007ef5a39b120632ae2e1d0514270791fcdd0aef70d4b6577a94e9e16c8519c7ec69e187f5fdd6e50b39c47532c69bc9696ee9c99f8d2e
7
+ data.tar.gz: fdabdef45c309724b83228b1ad72b49d2e62c5e0ad5c499d20981233ce927ea26d6ffe134fe6b49dc08ac12ef00d79866d49700a3e7c923760f6ebfa764f6f71
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 ZRealm
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/PRIVACY.md ADDED
@@ -0,0 +1,61 @@
1
+ # Privacy Notice
2
+
3
+ **ZMediumToMarkdown · Privacy `v1`** — what this CLI does and does not collect.
4
+
5
+ This Notice covers only the `ZMediumToMarkdown` Ruby gem (the **"Software"**). It does not cover Medium, GitHub, RubyGems, Cloudflare, or any other third-party service the Software contacts on your behalf — those services have their own privacy policies, and you are bound by them when you choose to interact with them.
6
+
7
+ ## 1. Scope
8
+
9
+ The Software runs entirely on **your** machine, executed by **your** Ruby interpreter, under **your** user account. The Author has no server, no analytics endpoint, no telemetry channel, and receives **no data** from your runs.
10
+
11
+ ## 2. What the Software does NOT collect
12
+
13
+ - **No analytics.** The Software does not phone home, does not record runs, does not measure usage, and does not generate any kind of identifier that could be sent to the Author.
14
+ - **No telemetry.** No crash reports, no performance metrics, no anonymous statistics.
15
+ - **No transmission of article content.** The Markdown the Software produces is written to your local filesystem (or your terminal in `--stdout` mode) and is never uploaded to anywhere by the Software.
16
+ - **No transmission of cookies to third parties.** Your Medium cookies (`sid`, `uid`, `cf_clearance`, `_cfuvid`) are sent **only** to `medium.com` itself, exactly as they would be in a normal browser request.
17
+
18
+ ## 3. Local data the Software stores
19
+
20
+ | Path | What it is | When it is written |
21
+ |---|---|---|
22
+ | `~/.zmediumtomarkdown/cookies.json` (or similar) | AES-256-GCM encrypted cache of your Medium cookies. The encryption key is derived per machine; the file is readable only by your user account. | When you supply cookies via CLI flags / env vars / `--auth`. |
23
+ | `~/.zmediumtomarkdown/.tos-v1-accepted` | Plain-text marker recording the ISO-8601 timestamp of your first-run consent and the `Terms` version. | When you type `yes` at the first-run prompt or pass `--accept-terms`. |
24
+ | Output directory under your `cwd` | Markdown files and downloaded images. | Whenever you invoke a download (`-p` / `-u`, no `--stdout`). |
25
+
26
+ You can delete any of these at any time. Removing the `.tos-vN-accepted` file restarts the consent prompt on the next run.
27
+
28
+ ## 4. Network calls the Software makes
29
+
30
+ When you invoke a download or render command, the Software contacts the following hosts. None of these calls are visible to the Author; they go directly from your machine to the listed hosts.
31
+
32
+ | Host | Purpose | Cookies attached |
33
+ |---|---|---|
34
+ | `medium.com/_/graphql` | Fetch post metadata + body via Medium's internal GraphQL endpoint. | Yes — your `sid` / `uid` / `cf_clearance` / `_cfuvid`. |
35
+ | `miro.medium.com/<imageId>` | Download article images. | No (Medium's image CDN does not require auth). |
36
+ | `cdn.syndication.twitter.com` | Expand embedded tweets into a Markdown blockquote (best-effort). | No. |
37
+ | `gist.github.com` / `gist.githubusercontent.com` | Expand embedded GitHub Gists into Markdown code blocks. | No. |
38
+ | `api.github.com/repos/ZhgChgLi/ZMediumToMarkdown/releases` | Check whether a newer gem version is available; printed as a one-line nudge. | No. |
39
+
40
+ If you configure a Cloudflare Worker proxy via `MEDIUM_HOST` (the `--medium_host` CLI flag), the GraphQL call is routed through that origin instead. **You** own and operate that Worker; its logging is governed by your Cloudflare account settings.
41
+
42
+ ## 5. Cloudflare Worker proxy (optional)
43
+
44
+ If you deploy the optional Cloudflare Worker proxy described in [`wiki/Setting-Up-Medium-Cookies-and-a-Cloudflare-Worker-Proxy.md`](./wiki/Setting-Up-Medium-Cookies-and-a-Cloudflare-Worker-Proxy.md), Cloudflare may log request metadata (timestamps, IPs, headers) to your Cloudflare account dashboard per its default behavior. The Author does not have access to that data. Configure retention and logging on your own Cloudflare side.
45
+
46
+ ## 6. Children's privacy
47
+
48
+ The Software is not directed at children under the age of thirteen (13). The Author does not knowingly collect any data from anyone, but the Software's intended users are adults capable of agreeing to its [Terms of Use](./TERMS.md).
49
+
50
+ ## 7. Changes to this Notice
51
+
52
+ Material changes are signaled by bumping the `Privacy` version string in this document and in `lib/Terms.rb`. The CLI will surface the bump on the next run.
53
+
54
+ ## 8. Contact
55
+
56
+ Questions about this Notice: GitHub Issues at [https://github.com/ZhgChgLi/ZMediumToMarkdown/issues](https://github.com/ZhgChgLi/ZMediumToMarkdown/issues).
57
+
58
+ ---
59
+
60
+ **Last updated:** 2026-05-10
61
+ **Version:** `v1`
data/README.md ADDED
@@ -0,0 +1,321 @@
1
+ # ZMediumToMarkdown
2
+
3
+ ![ZMediumToMarkdown](https://user-images.githubusercontent.com/33706588/184416147-c2ec74d4-7107-484e-8ad2-302340cf6c1f.png)
4
+
5
+ Download Medium posts as clean Markdown, preserving structure, images, links, code blocks, and common embeds for plain Markdown or Jekyll workflows.
6
+
7
+ [![Gem](https://badge.fury.io/rb/ZMediumToMarkdown.svg)](https://rubygems.org/gems/ZMediumToMarkdown)
8
+
9
+ ## Try it in 30 seconds
10
+
11
+ ```bash
12
+ gem install ZMediumToMarkdown
13
+ ZMediumToMarkdown -p "https://medium.com/<USER>/<POST>"
14
+ ```
15
+
16
+ The converted Markdown is written to `./Output/zmediumtomarkdown/`. Public posts usually work without cookies.
17
+
18
+ For **paywalled posts**, **bulk downloads**, or **CI / GitHub Actions**, you'll need Medium login cookies. On a local TTY the tool can auto-capture them by opening Chrome the first time Cloudflare blocks; CI runs need a Cloudflare Worker proxy. See [Cookies & Cloudflare setup](#cookies--cloudflare-setup).
19
+
20
+ > 📘 **[Setting Up Medium Cookies and a Cloudflare Worker Proxy →](https://github.com/ZhgChgLi/ZMediumToMarkdown/blob/main/wiki/Setting-Up-Medium-Cookies-and-a-Cloudflare-Worker-Proxy.md)**
21
+
22
+ ---
23
+
24
+ ## Features
25
+
26
+ - Convert one Medium post or download every post from a Medium username.
27
+ - Preserve headings, blockquotes, lists, inline code, fenced code blocks, images, links, and front matter.
28
+ - Render common embeds: GitHub Gists, Twitter / X, YouTube, Vimeo, SoundCloud, Spotify, and generic OG-image cards.
29
+ - Download images locally and emit paths for either plain Markdown output or Jekyll projects.
30
+ - Read paywalled posts when valid Medium `sid` / `uid` cookies (Membership account) are provided.
31
+ - Auto-capture login cookies via Chrome on a local TTY when Cloudflare blocks, into an encrypted on-disk cache reused on subsequent runs.
32
+ - Skip unchanged posts by comparing `last_modified_at`, making scheduled backups practical.
33
+ - Keep multilingual text stable, including CJK, Arabic, Hebrew, Cyrillic, and emoji.
34
+ - Stream rendered Markdown to stdout for embedding callers (e.g. [mcp-medium-reader](https://github.com/ZhgChgLi/mcp-medium-reader)) via `--stdout` / `--list`, no filesystem writes.
35
+ - Run as a Ruby gem, local CLI tool, or GitHub Action.
36
+
37
+ ---
38
+
39
+ ## Cookies & Cloudflare setup
40
+
41
+ Medium's GraphQL endpoint is protected by Cloudflare. Two failure modes interrupt a run:
42
+
43
+ 1. **Cloudflare bot challenge** — HTTP 403 / "Just a moment…". Empirically: after ~10 posts without cookies, or ~25 posts from CI / datacenter IPs without a Worker proxy.
44
+ 2. **Paywalled posts** — return only the public preview unless `sid` / `uid` come from a logged-in Medium **Member** account.
45
+
46
+ ### What you need by scenario
47
+
48
+ | Scenario | `sid` / `uid` cookies | Cloudflare Worker proxy |
49
+ |---|---|---|
50
+ | CI / CD (GitHub Actions, cloud runners) | **Strongly recommended** | **Strongly recommended** |
51
+ | Local machine (laptop / desktop) | Recommended for paywalled posts | Optional |
52
+ | Paywalled posts (anywhere) | **Required** (Membership account) | Independent |
53
+
54
+ ### Three ways to clear a Cloudflare block
55
+
56
+ 1. **Auto-login on a TTY (local).** When Cloudflare blocks an interactive run and Google Chrome is installed, the tool opens Chrome at <https://medium.com>; sign in / clear the challenge, and `sid` / `uid` / `cf_clearance` / `_cfuvid` are captured into an AES-256-GCM-encrypted cache at `~/.zmediumtomarkdown` (chmod 0600). Cached cookies are reused on subsequent runs and refreshed on every new block, so you rarely repeat the flow. Run `ZMediumToMarkdown --auth` once to trigger this flow on demand and seed the cache before any real run. Pass `--non-interactive` (or set `MEDIUM_NO_AUTO_BROWSER=1`) to suppress the prompt and fail fast.
57
+ 2. **Cloudflare Worker proxy.** Permanent fix, recommended for CI. Point the GraphQL endpoint (and optionally the image CDN) at your own Worker so requests originate from inside Cloudflare's network instead of a flagged datacenter IP.
58
+ 3. **Manual `cf_clearance` / `_cfuvid` cookies.** Short-term unblocking (~30 min). Useful when you can't run Chrome and don't want to set up a Worker proxy yet.
59
+
60
+ ### Inputs
61
+
62
+ CLI flag wins over env var, env var wins over the on-disk cache.
63
+
64
+ | Cookie / variable | CLI flag | Env var |
65
+ |---|---|---|
66
+ | `sid` (Medium login) | `-s, --cookie_sid` | `MEDIUM_COOKIE_SID` |
67
+ | `uid` (Medium login) | `-d, --cookie_uid` | `MEDIUM_COOKIE_UID` |
68
+ | `cf_clearance` | `--cookie_cf_clearance` | `MEDIUM_COOKIE_CF_CLEARANCE` |
69
+ | `_cfuvid` | `--cookie_cfuvid` | `MEDIUM_COOKIE_CFUVID` |
70
+ | Worker proxy host | `-x, --medium_host` | `MEDIUM_HOST` — set to your Worker URL with or without a `/_/graphql` suffix; the gem only uses the origin and rebuilds paths. Covers both medium.com and miro.medium.com via path dispatch. |
71
+ | Worker shared secret | — | `MEDIUM_HOST_SECRET` (sent as `X-Medium-Proxy-Secret` header on proxy requests; matches the `SECRET` constant in the Worker script) |
72
+
73
+ ```bash
74
+ # Env-var form (preferred — keeps secrets out of shell history)
75
+ export MEDIUM_COOKIE_SID="<your sid>"
76
+ export MEDIUM_COOKIE_UID="<your uid>"
77
+ ZMediumToMarkdown -p "https://medium.com/..."
78
+
79
+ # Or as flags for one-off runs
80
+ ZMediumToMarkdown -p "https://medium.com/..." -s "<sid>" -d "<uid>"
81
+
82
+ # Behind a single Cloudflare Worker that handles both medium.com and miro.medium.com.
83
+ # Either form of MEDIUM_HOST works — the gem only uses the origin.
84
+ export MEDIUM_HOST="https://my-worker.my-account.workers.dev/"
85
+ export MEDIUM_HOST_SECRET="<your-secret>"
86
+ ZMediumToMarkdown -u zhgchgli
87
+ ```
88
+
89
+ ### Full setup guide
90
+
91
+ The setup guide covers cookie extraction, Cloudflare Worker deployment, security notes, and GitHub Actions wiring:
92
+
93
+ > **[Setting Up Medium Cookies and a Cloudflare Worker Proxy](https://github.com/ZhgChgLi/ZMediumToMarkdown/blob/main/wiki/Setting-Up-Medium-Cookies-and-a-Cloudflare-Worker-Proxy.md)**
94
+
95
+ ---
96
+
97
+ ## Installation
98
+
99
+ ### Gem (recommended)
100
+
101
+ ```bash
102
+ gem install ZMediumToMarkdown
103
+ ```
104
+
105
+ On macOS, prefer a managed Ruby (`rbenv` / `rvm` / `asdf`) over the system Ruby. Installing gems against `/usr/bin/ruby` usually requires `sudo` and modifies the OS Ruby environment.
106
+
107
+ ### From source
108
+
109
+ ```bash
110
+ git clone https://github.com/ZhgChgLi/ZMediumToMarkdown
111
+ cd ZMediumToMarkdown
112
+ bundle install
113
+ bundle exec ruby bin/ZMediumToMarkdown -p "https://medium.com/..."
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Usage
119
+
120
+ ```
121
+ ZMediumToMarkdown [options]
122
+
123
+ -s, --cookie_sid SID Medium logged-in cookie sid (or $MEDIUM_COOKIE_SID)
124
+ -d, --cookie_uid UID Medium logged-in cookie uid (or $MEDIUM_COOKIE_UID)
125
+ --cookie_cf_clearance VALUE Cloudflare cf_clearance cookie (or $MEDIUM_COOKIE_CF_CLEARANCE).
126
+ Short-term Cloudflare unblocking; expires ~30 min.
127
+ --cookie_cfuvid VALUE Cloudflare _cfuvid cookie (or $MEDIUM_COOKIE_CFUVID).
128
+ Companion to cf_clearance.
129
+ -x, --medium_host URL Cloudflare Worker proxy URL (or $MEDIUM_HOST). One Worker
130
+ covers both medium.com and miro.medium.com via path
131
+ dispatch. Set $MEDIUM_HOST_SECRET to the same secret used
132
+ in the Worker script. Strongly recommended for CI / bulk
133
+ runs — see the wiki setup guide.
134
+ --non-interactive Never prompt or open Chrome on a Cloudflare block. CI runners
135
+ auto-detect this; use the flag to force the same behavior on a TTY.
136
+ --auth Open Chrome to sign in, capture cookies into the encrypted
137
+ cache (~/.zmediumtomarkdown), and exit. Run once before bulk /
138
+ scheduled jobs to seed the cache up front.
139
+ -u, --username USERNAME Download every post by a Medium username
140
+ -p, --postURL POST_URL Download a single post URL
141
+ --jekyll Emit Jekyll-friendly output (combine with -u or -p)
142
+ --stdout Render Markdown to stdout; skip all image/asset downloads.
143
+ Use with -p or -u. Logs and banners go to stderr.
144
+ --list With -u, emit one NDJSON line per post (title, url, dates,
145
+ tags, etc.) to stdout. Skips bodies and image downloads.
146
+ --limit N Cap the number of posts processed by -u in --stdout / --list.
147
+ -n, --new Update to the latest version (gem install only)
148
+ -c, --clean Remove every downloaded post under cwd
149
+ -v, --version Print the current version
150
+ -h, --help Show this message
151
+ ```
152
+
153
+ ### Examples
154
+
155
+ ```bash
156
+ # Single post into ./Output/zmediumtomarkdown/
157
+ ZMediumToMarkdown -p "https://medium.com/<user>/<slug>-<id>"
158
+
159
+ # Every post by a user, Jekyll-friendly into ./_posts/zmediumtomarkdown/ + ./assets/
160
+ ZMediumToMarkdown -u zhgchgli --jekyll
161
+ ```
162
+
163
+ For paywalled / bulk / CI runs, also pass cookies and (optionally) a Worker proxy — see [Cookies & Cloudflare setup](#cookies--cloudflare-setup).
164
+
165
+ > **Deprecated flags.** `-j USERNAME` and `-k POST_URL` still work for backwards compatibility but emit a warning. Use `--jekyll -u …` / `--jekyll -p …` instead.
166
+
167
+ ### Output layout
168
+
169
+ | Mode | Markdown destination | Image destination |
170
+ |---|---|---|
171
+ | Plain (`-p` / `-u`) | `./Output/zmediumtomarkdown/<date>-<slug>.md` | `./Output/zmediumtomarkdown/assets/<post_id>/` |
172
+ | Jekyll (`--jekyll`) | `./_posts/zmediumtomarkdown/<date>-<post_id>.md` | `./assets/<post_id>/` |
173
+
174
+ When run with `-u`, plain mode additionally nests under `./Output/users/<username>/`.
175
+
176
+ Reruns are cheap — posts whose `last_modified_at` matches the existing front matter are skipped.
177
+
178
+ ---
179
+
180
+ ## Embedding callers — `--stdout` / `--list`
181
+
182
+ The gem can also be invoked as a backend for tools that need rendered Markdown without filesystem side effects — most notably the [`mcp-medium-reader`](https://github.com/ZhgChgLi/mcp-medium-reader) MCP server, which exposes Medium reading to LLMs.
183
+
184
+ In `--stdout` / `--list` mode:
185
+
186
+ - Markdown / NDJSON is written to **stdout**; banners, progress, and warnings go to **stderr**.
187
+ - **No filesystem writes**, no `Output/` directory, no `assets/` directory.
188
+ - **No image downloads** — image references stay as remote URLs on `miro.medium.com` (or your `MEDIUM_HOST` proxy origin when configured).
189
+ - Skip-already-downloaded checks are bypassed; the post is rendered fresh every time.
190
+
191
+ ```bash
192
+ # Stream a single post's Markdown to stdout.
193
+ ZMediumToMarkdown --stdout -p "https://medium.com/<user>/<slug>-<id>"
194
+
195
+ # Stream every post by a user, separated by `\n\n---\n\n`. --limit caps the count.
196
+ ZMediumToMarkdown --stdout -u zhgchgli --limit 5
197
+
198
+ # List a user's posts as NDJSON (one JSON object per line). No bodies.
199
+ ZMediumToMarkdown --list -u zhgchgli --limit 20
200
+ # {"title":"…","url":"…","creator":"…","firstPublishedAt":"…","latestPublishedAt":"…","tags":["…"],"description":"…","pin":false}
201
+ ```
202
+
203
+ Cookies and Worker-proxy env vars apply the same way as in normal mode.
204
+
205
+ ---
206
+
207
+ ## Quick-start templates
208
+
209
+ - **GitHub Actions backup, no code**: [How-to walkthrough](https://github.com/ZhgChgLi/ZMediumToMarkdown/blob/main/wiki/Setting-Up-Medium-Cookies-and-a-Cloudflare-Worker-Proxy.md)
210
+ - **Working Action repo example**: <https://github.com/ZhgChgLi/ZMediumToMarkdown-github-action>
211
+
212
+ ### Minimal GitHub Action
213
+
214
+ ```yaml
215
+ name: ZMediumToMarkdown
216
+ on:
217
+ workflow_dispatch:
218
+ schedule:
219
+ - cron: "10 1 15 * *" # 01:10 on day-of-month 15
220
+ jobs:
221
+ backup:
222
+ runs-on: ubuntu-latest
223
+ steps:
224
+ - uses: ZhgChgLi/ZMediumToMarkdown@main
225
+ env:
226
+ MEDIUM_COOKIE_SID: ${{ secrets.MEDIUM_COOKIE_SID }}
227
+ MEDIUM_COOKIE_UID: ${{ secrets.MEDIUM_COOKIE_UID }}
228
+ with:
229
+ command: "-u zhgchgli"
230
+ ```
231
+
232
+ Store `MEDIUM_COOKIE_SID` / `MEDIUM_COOKIE_UID` as repository **secrets**, not repository variables, and never hard-code them in YAML. Pass them through the step's `env:` block instead of the `command:` string so they stay out of logs. For CI, also point `MEDIUM_HOST` at a Cloudflare Worker proxy; see the [setup guide](https://github.com/ZhgChgLi/ZMediumToMarkdown/blob/main/wiki/Setting-Up-Medium-Cookies-and-a-Cloudflare-Worker-Proxy.md).
233
+
234
+ ---
235
+
236
+ ## Example output
237
+
238
+ - [Original post on Medium](https://medium.com/zrealm-ios-dev/avplayer-%E5%AF%A6%E8%B8%90%E6%9C%AC%E5%9C%B0-cache-%E5%8A%9F%E8%83%BD%E5%A4%A7%E5%85%A8-6ce488898003)
239
+ - [Converted Markdown output](example/2021-01-31-avplayer-實踐本地-cache-功能大全-6ce488898003.md)
240
+
241
+ ---
242
+
243
+ ## Troubleshooting
244
+
245
+ | Symptom | Likely cause | Fix |
246
+ |---|---|---|
247
+ | `Blocked by Medium's Cloudflare layer (HTTP 403)` | Cloudflare bot challenge; common after about 10 posts without cookies, or about 25 posts from CI / datacenter IPs without a Worker proxy | **Local**: on a TTY the tool auto-opens Chrome to clear the challenge and refresh cookies (cached at `~/.zmediumtomarkdown`). If Chrome is not installed, open <https://medium.com> in any browser, clear the challenge, then rerun. **CI / datacenter**: set up cookies and a Cloudflare Worker proxy — see the [setup guide](https://github.com/ZhgChgLi/ZMediumToMarkdown/blob/main/wiki/Setting-Up-Medium-Cookies-and-a-Cloudflare-Worker-Proxy.md). |
248
+ | `This post is behind Medium's paywall…` even though I set cookies | Cookies do not belong to a Medium **Member** account that can read this post, or they have expired after inactivity | Refresh `sid` / `uid` from a logged-in browser and verify the account has access to the post. Cookies stay valid as long as they keep being used. |
249
+ | `Error: Too Many Requests, blocked by Medium` | Hit Medium’s rate limit | Slow the schedule down or split the run; the tool already retries up to 10 times. |
250
+ | Markdown looks fine but CJK / emoji is mojibaked | Older release — encoding regression | Upgrade to ≥ 2.6.7 (this release force-encodes all responses to UTF-8). |
251
+ | `An iframe came back blank` | Generic embed (non-Twitter, non-gist, non-YouTube, non-widgetic) without an OG image | Expected — the source has no image to embed. The tool emits an empty line so paragraph spacing is preserved. |
252
+
253
+ ---
254
+
255
+ ## Development
256
+
257
+ ```bash
258
+ bundle install
259
+ bundle exec rake test # run the minitest suite
260
+ ```
261
+
262
+ The suite includes a 174-paragraph end-to-end fixture under `test/fixtures/`. To regenerate the golden Markdown file after intentional output changes:
263
+
264
+ ```bash
265
+ UPDATE_FIXTURES=1 bundle exec rake test
266
+ ```
267
+
268
+ CI runs the same `rake test` against Ruby 3.2 / 3.3 / 3.4.
269
+
270
+ ---
271
+
272
+ ## Disclaimer
273
+
274
+ ### Medium's Terms of Service
275
+
276
+ Medium's official Terms of Service forbid using *"any software, script, robot, spider or other automatic device, process or means (including crawlers, browser plugins and add-ons or any other technology) to access the Services."* — [Medium Rules](https://policy.medium.com/medium-rules-30e5502c4eb4).
277
+
278
+ ZMediumToMarkdown is exactly the kind of tool that statement contemplates. **Use of this gem may conflict with Medium's Terms of Service.** The author makes no claim that this is permitted use; you accept all risk, including account suspension, IP-address blocks, or legal action by Medium. On its first invocation the CLI prints a one-time consent prompt and requires you to type `yes` before any network call is made; in non-interactive environments set `ZMTM_TOS_ACCEPTED=1` or pass `--accept-terms` once.
279
+
280
+ ### Copyright
281
+
282
+ All content downloaded using ZMediumToMarkdown — articles, images, video — is subject to copyright and belongs to its respective owner. This tool does not claim ownership of any downloaded content.
283
+
284
+ Downloading and using copyrighted content without the owner's permission may be illegal. ZMediumToMarkdown does not condone copyright infringement and will not be held responsible for misuse of this tool. Users are solely responsible for ensuring they have the necessary permissions and rights for any content they download.
285
+
286
+ By using ZMediumToMarkdown you acknowledge and agree to comply with all applicable copyright laws and regulations.
287
+
288
+ ### Full Terms
289
+
290
+ The complete Terms of Use are in [TERMS.md](./TERMS.md); the privacy posture is in [PRIVACY.md](./PRIVACY.md). Both are versioned — when they change in a backwards-incompatible way, the CLI invalidates the existing consent marker and re-prompts on the next run.
291
+
292
+ ---
293
+
294
+ ## Other works
295
+
296
+ **Ruby libraries**
297
+ - [RubyRangeable](https://github.com/ZhgChgLi/RubyRangeable) — generic interval-set container, published as the `rangeable` gem; powers ZMediumToMarkdown's markup rendering since 3.6.0. Spec: [RangeableRFC](https://github.com/ZhgChgLi/RangeableRFC).
298
+
299
+ **Swift libraries**
300
+ - [ZMarkupParser](https://github.com/ZhgChgLi/ZMarkupParser) — pure-Swift HTML → `NSAttributedString` with customizable style/tag mapping.
301
+ - [ZPlayerCacher](https://github.com/ZhgChgLi/ZPlayerCacher) — lightweight `AVAssetResourceLoaderDelegate` cache for `AVPlayerItem` streaming.
302
+ - [SwiftRangeable](https://github.com/ZhgChgLi/SwiftRangeable) — Swift reference implementation of Rangeable.
303
+
304
+ **Integration tools**
305
+ - [mcp-medium-reader](https://github.com/ZhgChgLi/mcp-medium-reader) — macOS MCP server that wraps this gem so LLMs (Claude Desktop, etc.) can read Medium posts.
306
+ - [XCFolder](https://github.com/ZhgChgLi/XCFolder) — convert Xcode virtual groups to real directories (Tuist / XcodeGen friendly).
307
+ - [ZReviewTender](https://github.com/ZhgChgLi/ZReviewTender) — fetch App Store / Google Play reviews into your workflow.
308
+ - [linkyee](https://github.com/ZhgChgLi/linkyee) — open-source LinkTree alternative on GitHub Pages.
309
+
310
+ ---
311
+
312
+ ## About
313
+
314
+ - <https://zhgchg.li/>
315
+ - <https://blog.zhgchg.li/>
316
+
317
+ ## Donate
318
+
319
+ [![Buy Me A Beer](https://github.com/user-attachments/assets/63f01edf-2aa5-4d91-8f8a-861e5b6b4feb)](https://www.paypal.com/ncp/payment/CMALMPT8UUTY2)
320
+
321
+ If this project helped you, please star the repo or [buy me a beer](https://www.paypal.com/ncp/payment/CMALMPT8UUTY2). PRs and issue reports welcome.
data/TERMS.md ADDED
@@ -0,0 +1,109 @@
1
+ # Terms of Use
2
+
3
+ **ZMediumToMarkdown · Terms `v1`**
4
+
5
+ These Terms govern your use of the `ZMediumToMarkdown` Ruby gem and any related code in this repository (collectively, **"the Software"**). The Software is offered by **ZhgChgLi** (an individual, identified by the GitHub handle [`@ZhgChgLi`](https://github.com/ZhgChgLi); referred to here as **"the Author"**) free of charge under the MIT License.
6
+
7
+ By installing, running, or otherwise using the Software you agree to these Terms. The CLI will, on its first invocation, print a one-time consent prompt and require an explicit `yes` before performing any network call. If you do not agree, do not use the Software.
8
+
9
+ ---
10
+
11
+ ## 1. What the Software is
12
+
13
+ The Software is a personal command-line utility, written in Ruby, that converts a Medium post into a Markdown file by issuing HTTP requests to Medium's GraphQL endpoint using cookies that **you supply** from your own browser session. It is open source under MIT, with all source code public on [GitHub](https://github.com/ZhgChgLi/ZMediumToMarkdown).
14
+
15
+ The Software is **not affiliated with, endorsed by, sponsored by, or connected to** Medium, A Medium Corporation, or any of its subsidiaries.
16
+
17
+ ## 2. Third-party services risk · Medium's Terms of Service
18
+
19
+ Medium's official rules forbid automated access to its services. The relevant text, quoted from [Medium Rules](https://policy.medium.com/medium-rules-30e5502c4eb4), reads:
20
+
21
+ > "any software, script, robot, spider or other automatic device, process or means (including crawlers, browser plugins and add-ons or any other technology) to access the Services for any purpose, including without limitation to scrape or otherwise copy any of the data or content on the Services."
22
+
23
+ The Software is exactly the kind of tool that statement contemplates.
24
+
25
+ **The Author makes no claim that this use is permitted by Medium.** Using the Software may technically conflict with Medium's Terms of Service. You are using it **at your own risk** and accept full responsibility for any consequence, including but not limited to:
26
+
27
+ - account warning, suspension, or termination by Medium;
28
+ - IP address rate-limiting or blocking;
29
+ - legal action by Medium or by the original article author(s);
30
+ - loss of access to articles you previously could read.
31
+
32
+ If you are unsure whether your intended use is acceptable, do not run the Software — use Medium's own official export feature ([Settings → Account → Download your information](https://help.medium.com/hc/en-us/articles/115004551948)) instead.
33
+
34
+ ## 3. Eligibility
35
+
36
+ You may only use the Software if you are old enough to enter into a binding agreement under the law of the jurisdiction in which you reside (typically thirteen (13) years of age or older, sixteen (16) in some countries, eighteen (18) where the law requires).
37
+
38
+ ## 4. License & open source
39
+
40
+ The Software is released under the **MIT License**. The full license text is in [`LICENSE`](./LICENSE). The MIT License grants you broad rights to use, copy, modify, and redistribute the Software's source code; it does **not** grant any rights to the article content, images, embeds, or other materials the Software downloads from Medium.
41
+
42
+ ## 5. Your responsibilities
43
+
44
+ By using the Software you agree that:
45
+
46
+ 1. **Access right.** You only convert articles that you have the legitimate right to read on Medium — typically because the article is publicly accessible, or because you are a Medium Member with active access to the article's metering tier.
47
+ 2. **Cookies are yours.** Any `sid`, `uid`, `cf_clearance`, or `_cfuvid` value you supply belongs to a Medium account that you yourself control. You will not use someone else's session.
48
+ 3. **No mass scraping.** You will not use the Software for bulk crawling, building large datasets for resale, training datasets without independent permission from the rights holders, or any commercial redistribution.
49
+ 4. **Respect copyright.** Any Markdown the Software produces remains the original Medium author's copyrighted work. You will only redistribute, republish, or otherwise share the output where the original author has granted you permission, or where the law of your jurisdiction (fair use, fair dealing, quotation, etc.) clearly permits it.
50
+ 5. **Your platform, your call.** You are responsible for compliance with Medium's Terms of Service, with applicable computer-fraud and access-control laws, and with any contractual obligation you may have toward third parties (employer policies, NDA, etc.).
51
+
52
+ ## 6. Intellectual property
53
+
54
+ The Software's source code is © `ZhgChgLi`, licensed under MIT. The article content, images, video stills, code samples, and embeds the Software downloads belong to their respective rights holders. The existence of the Software does not transfer any ownership in those materials and does not imply a license to redistribute them.
55
+
56
+ ## 7. Disclaimer of warranties
57
+
58
+ THE SOFTWARE IS PROVIDED **"AS IS"** AND **"AS AVAILABLE"**, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, ACCURACY, AND NON-INFRINGEMENT. THE AUTHOR DOES NOT WARRANT THAT:
59
+
60
+ - the Software will run without errors or interruption;
61
+ - the conversion output will be complete, faithful, or lossless;
62
+ - the Software complies with Medium's Terms of Service or with any other third party's terms;
63
+ - the Software is fit for any particular regulatory environment.
64
+
65
+ ## 8. Limitation of liability
66
+
67
+ TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, CONSEQUENTIAL, SPECIAL, EXEMPLARY, OR PUNITIVE DAMAGES, OR ANY LOSS OF DATA, PROFITS, REVENUE, GOODWILL, OR ARTICLES, ARISING OUT OF OR IN CONNECTION WITH THE SOFTWARE, REGARDLESS OF THE LEGAL THEORY (CONTRACT, TORT, STRICT LIABILITY, OR OTHERWISE) AND EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
68
+
69
+ ## 9. Indemnification
70
+
71
+ You agree to defend, indemnify, and hold harmless the Author from and against any claim, demand, action, loss, or expense (including reasonable attorneys' fees) arising out of or related to:
72
+
73
+ - your breach of these Terms;
74
+ - your breach of Medium's Terms of Service through use of the Software;
75
+ - your infringement of any copyright, trade secret, or other right of any third party;
76
+ - your use of the Software in any manner that is unlawful in your jurisdiction.
77
+
78
+ ## 10. No support obligation
79
+
80
+ The Software is offered as a personal open-source project. The Author has no obligation to provide support, fix bugs, accept patches, answer questions, or maintain compatibility with future versions of Medium's API or website. Issues and pull requests on GitHub are reviewed on a best-effort, non-binding basis.
81
+
82
+ ## 11. Modifications & termination
83
+
84
+ The Author may modify these Terms at any time by publishing a new version in the repository. Material changes are signaled by bumping the `Terms` version string in `lib/Terms.rb`; on the next run, the CLI will print the updated summary and require fresh acceptance before proceeding.
85
+
86
+ The Author may, at any time and without notice, suspend, archive, or remove the Software (including unpublishing the gem from RubyGems and archiving the GitHub repository). You will not be entitled to any compensation if this occurs.
87
+
88
+ ## 12. Governing law & dispute resolution
89
+
90
+ The Software is provided **as-is** under the MIT License. **No specific jurisdiction is asserted.** Disputes arising from your use of the Software are governed by the law of the jurisdiction in which **you** reside, applied to the MIT License's existing "AS IS" provisions and to the disclaimers in Sections 7 – 9 above. The Author has no obligation to litigate, defend, or appear in any forum, and reserves the right to do nothing in response to any claim.
91
+
92
+ Where mandatory consumer-protection or copyright law of your jurisdiction grants you rights that cannot be waived by these Terms, those rights are unaffected.
93
+
94
+ ## 13. Severability
95
+
96
+ If any provision of these Terms is held invalid or unenforceable by a court of competent jurisdiction, that provision will be limited or removed to the minimum extent necessary, and the remainder of the Terms will continue in full force.
97
+
98
+ ## 14. Contact
99
+
100
+ The only supported channel for questions, takedown notices, or legal inquiries about the Software itself is GitHub Issues:
101
+
102
+ > [https://github.com/ZhgChgLi/ZMediumToMarkdown/issues](https://github.com/ZhgChgLi/ZMediumToMarkdown/issues)
103
+
104
+ For concerns about specific Medium articles or accounts, contact Medium directly — the Author does not host, store, or distribute Medium content.
105
+
106
+ ---
107
+
108
+ **Last updated:** 2026-05-10
109
+ **Version:** `v1`
@@ -14,8 +14,11 @@ quietBanner = ARGV.include?('--stdout') || ARGV.include?('--list')
14
14
  bannerOut = quietBanner ? $stderr : $stdout
15
15
 
16
16
  begin
17
- bannerOut.puts "#https://github.com/ZhgChgLi/ZMediumToMarkdown"
18
- bannerOut.puts "You have read and agree with the Disclaimer."
17
+ # Note: The first-run Terms-of-Use prompt + acceptance machinery now
18
+ # lives in `lib/Terms.rb` and is triggered from `CLI.main` only when the
19
+ # invocation actually contacts Medium. The previous unconditional
20
+ # "You have read and agree with the Disclaimer." line was removed — it
21
+ # implied consent that hadn't actually been collected.
19
22
 
20
23
  CLI.main(ARGV)
21
24
 
data/lib/CLI.rb CHANGED
@@ -7,6 +7,7 @@ require 'PathPolicy'
7
7
  require 'Request'
8
8
  require 'CookieCache'
9
9
  require 'ChromeAuth'
10
+ require 'Terms'
10
11
 
11
12
  # All CLI-side concerns for the `ZMediumToMarkdown` executable. Pulled out
12
13
  # of bin/ so it can be exercised by unit tests without spawning processes.
@@ -21,8 +22,19 @@ module CLI
21
22
  argv = argv.dup
22
23
  argv << '-h' if argv.empty?
23
24
 
25
+ # Strip --accept-terms / --decline-terms before OptionParser sees them
26
+ # (it would otherwise raise InvalidOption on these unknown flags).
27
+ Terms.consumeFlags!(argv, errput: errput)
28
+
24
29
  options = parseArgs(argv, errput: errput)
25
30
  loadCookies!
31
+
32
+ # Only block on Terms when the run will actually contact Medium.
33
+ # --help / --version / --clean / --new stay quick.
34
+ if willHitMedium?(options)
35
+ Terms.ensureAccepted!(errput: errput, input: $stdin)
36
+ end
37
+
26
38
  warnAboutMissingSetup(options, errput: errput)
27
39
  run(options, cwd, output: output, errput: errput)
28
40
  end
data/lib/Terms.rb ADDED
@@ -0,0 +1,127 @@
1
+ require 'fileutils'
2
+ require 'time'
3
+
4
+ # First-run consent gate for ZMediumToMarkdown.
5
+ #
6
+ # Workflow (priority high → low):
7
+ # 1. CLI flag `--accept-terms` (or `--decline-terms`) consumed before
8
+ # OptionParser, so it never sees them.
9
+ # 2. ENV var ZMTM_TOS_ACCEPTED=1 persistent CI escape.
10
+ # 3. Local marker file ~/.zmediumtomarkdown/.tos-vN written on first
11
+ # interactive accept.
12
+ # 4. Otherwise: print the summary and prompt on a TTY; refuse to run on
13
+ # a non-TTY (so a piped invocation can't accidentally bypass).
14
+ #
15
+ # Bumping `VERSION` invalidates every existing marker file, forcing every
16
+ # user to re-read and re-accept the new Terms.
17
+ module Terms
18
+ VERSION = 'v1'.freeze
19
+ ENV_OPT_IN = 'ZMTM_TOS_ACCEPTED'.freeze
20
+ ACCEPT_FLAG = '--accept-terms'.freeze
21
+ DECLINE_FLAG = '--decline-terms'.freeze
22
+
23
+ TERMS_URL = 'https://github.com/ZhgChgLi/ZMediumToMarkdown/blob/main/TERMS.md'.freeze
24
+ PRIVACY_URL = 'https://github.com/ZhgChgLi/ZMediumToMarkdown/blob/main/PRIVACY.md'.freeze
25
+
26
+ module_function
27
+
28
+ def acceptDir
29
+ File.join(Dir.home, '.zmediumtomarkdown')
30
+ end
31
+
32
+ def acceptPath(version = VERSION)
33
+ File.join(acceptDir, ".tos-#{version}-accepted")
34
+ end
35
+
36
+ # Strips --accept-terms / --decline-terms out of argv (in place) before
37
+ # OptionParser sees them. Side effects: writing the marker on accept,
38
+ # SystemExit on decline.
39
+ def consumeFlags!(argv, errput: $stderr)
40
+ if argv.delete(DECLINE_FLAG)
41
+ errput.puts 'Terms declined. Exiting.'
42
+ raise SystemExit.new(2)
43
+ end
44
+
45
+ if argv.delete(ACCEPT_FLAG)
46
+ writeAcceptance!
47
+ errput.puts "Terms #{VERSION} accepted (see #{acceptPath})."
48
+ end
49
+ end
50
+
51
+ # Block until the user has explicitly accepted these Terms. Called
52
+ # before any network operation (see CLI#main).
53
+ def ensureAccepted!(errput: $stderr, input: $stdin)
54
+ return if alreadyAccepted?
55
+
56
+ printSummary(errput)
57
+
58
+ unless input.respond_to?(:tty?) && input.tty?
59
+ errput.puts <<~MSG.chomp
60
+
61
+ [!] Cannot prompt for consent on a non-interactive stream.
62
+ Run this command in a terminal once, or set #{ENV_OPT_IN}=1,
63
+ or pass #{ACCEPT_FLAG} on the command line.
64
+ MSG
65
+ raise SystemExit.new(2)
66
+ end
67
+
68
+ errput.print "Type 'yes' to accept and continue: "
69
+ answer = input.gets
70
+ answer = answer.to_s.strip.downcase
71
+
72
+ if answer == 'yes' || answer == 'y'
73
+ writeAcceptance!
74
+ errput.puts "Accepted. (Marker written to #{acceptPath})"
75
+ else
76
+ errput.puts 'Declined. Exiting.'
77
+ raise SystemExit.new(2)
78
+ end
79
+ end
80
+
81
+ def alreadyAccepted?
82
+ return true if ENV[ENV_OPT_IN].to_s == '1'
83
+ File.exist?(acceptPath)
84
+ end
85
+
86
+ def writeAcceptance!
87
+ FileUtils.mkdir_p(acceptDir)
88
+ File.write(acceptPath, "accepted_at: #{Time.now.utc.iso8601}\nversion: #{VERSION}\n")
89
+ rescue StandardError => e
90
+ # Don't crash if the home directory is read-only — env var still works.
91
+ warn "[Terms] Could not write acceptance marker (#{e.class}: #{e.message}). " \
92
+ "Re-prompt will appear next run unless #{ENV_OPT_IN}=1."
93
+ end
94
+
95
+ def printSummary(io)
96
+ io.puts <<~MSG
97
+ ────────────────────────────────────────────────────────────────────
98
+ ZMediumToMarkdown — first-run notice (Terms #{VERSION})
99
+ ────────────────────────────────────────────────────────────────────
100
+ Medium's Terms of Service forbid automated access to its Services,
101
+ including via browser plugins, scripts, and CLI tools like this one.
102
+
103
+ Use of this tool may conflict with Medium's ToS. You are using it
104
+ AT YOUR OWN RISK. The author (ZhgChgLi) accepts no liability for
105
+ your use, including any account suspension, IP block, or legal
106
+ claim by Medium or by the original article authors.
107
+
108
+ By continuing you agree to:
109
+ • only convert articles you have legitimate access to read,
110
+ • respect the original author's copyright when redistributing,
111
+ • not use this tool for mass scraping or commercial redistribution.
112
+
113
+ Read the full Terms : #{TERMS_URL}
114
+ Read the Privacy : #{PRIVACY_URL}
115
+
116
+ (To bypass this prompt non-interactively, set #{ENV_OPT_IN}=1
117
+ or pass #{ACCEPT_FLAG} once.)
118
+ ────────────────────────────────────────────────────────────────────
119
+ MSG
120
+ end
121
+
122
+ # One-line reminder used by CLI banners, after the Terms have been
123
+ # accepted. Kept short.
124
+ def acceptedBannerLine
125
+ "ℹ By using this tool you agreed to the Terms (#{VERSION}) — #{TERMS_URL}"
126
+ end
127
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ZMediumToMarkdown
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.7.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ZhgChgLi
@@ -115,6 +115,10 @@ executables:
115
115
  extensions: []
116
116
  extra_rdoc_files: []
117
117
  files:
118
+ - LICENSE
119
+ - PRIVACY.md
120
+ - README.md
121
+ - TERMS.md
118
122
  - bin/ZMediumToMarkdown
119
123
  - lib/CLI.rb
120
124
  - lib/ChromeAuth.rb
@@ -148,6 +152,7 @@ files:
148
152
  - lib/Queries/UserFollowersQuery.graphql
149
153
  - lib/Queries/UserProfileQuery.graphql
150
154
  - lib/Request.rb
155
+ - lib/Terms.rb
151
156
  - lib/User.rb
152
157
  - lib/ZMediumFetcher.rb
153
158
  homepage: https://github.com/ZhgChgLi/ZMediumToMarkdown