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 +4 -4
- data/LICENSE +21 -0
- data/PRIVACY.md +61 -0
- data/README.md +321 -0
- data/TERMS.md +109 -0
- data/bin/ZMediumToMarkdown +5 -2
- data/lib/CLI.rb +12 -0
- data/lib/Terms.rb +127 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8b71926fd220e5524846da26d0ff13d8faf877a7320d40445c1e1561842fa125
|
|
4
|
+
data.tar.gz: 63910f5efe83d0ac58ff677374504b47455c0bb42b5d332273a40fa96fa687f1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
+

|
|
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
|
+
[](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
|
+
[](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`
|
data/bin/ZMediumToMarkdown
CHANGED
|
@@ -14,8 +14,11 @@ quietBanner = ARGV.include?('--stdout') || ARGV.include?('--list')
|
|
|
14
14
|
bannerOut = quietBanner ? $stderr : $stdout
|
|
15
15
|
|
|
16
16
|
begin
|
|
17
|
-
|
|
18
|
-
|
|
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:
|
|
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
|