A weekly newspaper for a cut-off Kindle

On 20 May 2026 Amazon ended support for every Kindle released in 2012 or earlier. The store is gone from those devices, and so is Send to Kindle. The BBC put the number affected at around two million.

Mine is a Kindle 4 non-touch from 2012. It still reads perfectly well. This turns it into something that receives a weekly newspaper built from RSS feeds, delivered over USB, with nothing left to do by hand except plug in the cable.

Reasoning: a cut-off Kindle has no resale value and cannot be given away, for a reason explained in step 1. It is also the only screen I own that is readable in direct sunlight and lasts weeks on a charge. Those two facts together make it worth ten minutes of setup rather than a drawer.

This runs on the desktop, not on a server. It pairs with adding a private RSS reader and self-hosting push notifications, though neither is required.

Contents:

1. Read this before touching the device
2. Identify the Kindle and back it up
3. Install calibre and an extractor
4. Extract the article text
5. Package it as a periodical
6. The build script
7. Deliver it on plug-in
8. Purge old issues without eating your books
9. Get told when it needs plugging in
10. What doesn't work any more
11. The script

1. Read this before touching the device

Never factory reset or deregister a cut-off Kindle. Amazon's own support page is explicit: an affected device that is reset or deregistered cannot be registered again. It stops working entirely. There is no recovery and no workaround.

This matters because almost every Kindle repurposing guide on the internet was written before May 2026, and a good number of them open by telling you to factory reset the device first. That instruction now destroys it.

The same fact means the device cannot be sold or passed on. A new owner would receive something they cannot register. It stays on your account permanently.

2. Identify the Kindle and back it up

Plug it in. A Kindle e-reader is not an Android device and does not speak ADB, whatever the search results suggest. It mounts as USB mass storage under vendor ID 1949 (Lab126):

๐Ÿ“‹
lsusb -v -d 1949: 2>/dev/null | grep -iE 'idProduct|iSerial'

The serial number's first four characters identify the model. Mine reads B023, which is the Kindle 4 non-touch, 2012 black refresh, model D01100. The firmware version is not exposed over USB, because only the user partition is mounted; read it from Settings on the device itself.

Find the mount point by filesystem label rather than assuming a path, since it differs between distributions:

๐Ÿ“‹
findmnt -rno TARGET -S LABEL=Kindle

On Debian and Ubuntu derivatives this returns something like /media/youruser/Kindle. On Fedora it is under /run/media/ instead. Every step below uses this lookup rather than a hardcoded path.

Back the books up now. They can no longer be re-downloaded from Amazon, so the copies on the device may be the only ones you have:

๐Ÿ“‹
KINDLE=$(findmnt -rno TARGET -S LABEL=Kindle)
cp -r "$KINDLE/documents" ~/kindle-backup

3. Install calibre and an extractor

Two tools, doing two different jobs. calibre packages the result into a periodical, which is what gives you feed-grouped navigation and per-article read tracking on the device. Something else has to turn each web page into readable text.

The obvious choice is calibre's own auto_cleanup, its built-in readability implementation. I used it first and it is not good enough. Measuring the delivered file, most articles were fine but nine of eighty-six were broken, and one Ars Technica article arrived carrying 77 links inside 5.4KB of text โ€” every icon and navigation element intact. The failure mode is specific: when the algorithm cannot identify the article body, it returns the whole page.

trafilatura does the job properly. On the same pages it reduced Ars Technica from 53,471 characters to 1,625, and IEEE Spectrum from 329,996 to 1,131.

๐Ÿ“‹
sudo apt-get install -y --no-install-recommends calibre

trafilatura goes in a virtualenv, for two reasons. Most distributions now mark the system Python as externally managed under PEP 668 and refuse pip install. More importantly, calibre ships its own Python interpreter โ€” import calibre fails from your system python3 โ€” so a calibre recipe cannot import trafilatura no matter where you install it. Extraction has to happen outside calibre. That turns out to be the right architecture anyway.

๐Ÿ“‹
python3 -m venv ~/kindle-news/venv
~/kindle-news/venv/bin/pip install trafilatura feedparser

4. Extract the article text

This is the stage that matters. It reads the feeds, reduces each article to its text, and throws away whatever fails. Save it as ~/kindle-news/extract.py.

Feeds live in a plain text file, ~/kindle-news/feeds.txt, rather than inside the Python. Adding one is then appending a line, which is also what lets the installer add feeds for you:

๐Ÿ“‹
# One feed per line:  Name | URL
Hacker News (best) | https://hnrss.org/best
Lobsters (top week) | https://lobste.rs/top/1w.rss
Ars Technica | https://feeds.arstechnica.com/arstechnica/index

The thresholds stay in the script:

๐Ÿ“‹
DAYS = 7
MAX_PER_FEED = 12
MIN_CHARS = 900    # below this it is a stub or a landing page
MAX_DENSITY = 0.15  # above this it is still page furniture
UA = ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
      "(KHTML, like Gecko) Chrome/120.0 Safari/537.36")

Fetch with a browser user-agent. Do not skip this. trafilatura's default user-agent is refused by a growing number of publishers, and the failure is silent and confusing because the feed parses fine while every article is blocked. The Critic blocked 12 of 12 articles with the default and passed 8 of 8 with a browser string.

๐Ÿ“‹
def fetch(url):
    req = urllib.request.Request(url, headers={
        "User-Agent": UA,
        "Accept-Language": "en-GB,en;q=0.9",
    })
    try:
        with urllib.request.urlopen(req, timeout=25) as r:
            enc = r.headers.get_content_charset() or "utf-8"
            return r.read().decode(enc, "replace")
    except Exception:
        return trafilatura.fetch_url(url)

Measure quality with link density โ€” the share of an article's text that sits inside <a> tags. This one number separates prose from page furniture reliably: clean articles sit between 0.00 and 0.06, whole-page dumps between 0.15 and 0.99. It is how you diagnose the problem and then prove you have fixed it.

๐Ÿ“‹
def density(h):
    text = re.sub(r"\s+", " ", html.unescape(re.sub(r"<[^>]+>", " ", h))).strip()
    atext = " ".join(re.sub(r"<[^>]+>", "", m)
                for m in re.findall(r"<a\s[^>]*>(.*?)</a>", h, re.S))
    atext = re.sub(r"\s+", " ", html.unescape(atext)).strip()
    return len(atext) / max(len(text), 1), len(text)

Then extract, with a gate. Where the feed already carries the full article in content:encoded, use it and skip the fetch entirely โ€” it is faster, politer, and removes a chance to fail:

๐Ÿ“‹
def extract(entry):
    url = entry.get("link")
    src = embedded(entry) or fetch(url)
    if src is None:
        return None, "fetch blocked"
    out = trafilatura.extract(src, output_format="html", url=url,
                          include_comments=False, favor_precision=True)
    if not out:
        return None, "no article found"
    d, chars = density(out)
    if chars < MIN_CHARS:
        return None, f"too short ({chars}c)"
    if d > MAX_DENSITY:
        return None, f"still furniture ({d:.2f})"
    return out, "ok"

Drop what fails rather than shipping it. A repository page linked from Hacker News is not an article and no extractor will turn it into one. On a device whose browser cannot load anything, a title with a dead link is worthless.

The 900-character floor is worth tuning rather than copying. Real War on the Rocks articles extract at 12,000 to 21,000 characters, while its podcast pages come in at 760. An earlier floor of 1,200 was wrongly rejecting legitimate short news items at around 1,130.

Finally, write the results out as JSON for the next stage, with a sections list of {title, articles}, each article carrying title, url, date and the extracted HTML as description. Deduplicate on normalised URL and lower-cased title while you go, because aggregators surface the same links repeatedly. A ThreadPoolExecutor with eight workers keeps a hundred fetches to about fifteen seconds.

Test every feed before you trust it. This is the part I would most want to have known at the start. A feed can return a perfectly healthy list of articles whose pages contain no readable text whatsoever, and you will not discover it until you are holding the Kindle. Give extract.py a --check mode that runs a feed through the same code the pipeline uses:

๐Ÿ“‹
~/kindle-news/venv/bin/python ~/kindle-news/extract.py --check <url>

It fetches the feed, counts the items, then puts the first three articles through the real extractor and reports what happened. A good source looks like this:

๐Ÿ“‹
  feed OK: 100 items
    ok    The intractable problems pulling modern Brit   11236c density 0.00
    ok    Burnham's utilities fantasy    5475c density 0.00
  GOOD    articles extract cleanly. Safe to add.

And an unusable one looks like this. The feed is fine, twenty items, correct titles โ€” and not one article can be read:

๐Ÿ“‹
  feed OK: 20 items
    FAIL  Houses of Worship Deserve Protection from Di   no article found
  UNUSABLE  no article could be extracted.
          The pages carry no article text. Sites that render their text in
          JavaScript look complete but are empty to any extractor.

Distinguishing the three failure modes is what makes the output worth printing. Fetch blocked means the publisher refuses you and there is nothing to do. No article found means the text is rendered in JavaScript and no extractor will ever see it. Too short usually means a paywall or a feed of teasers. Only the first is worth retrying.

5. Package it as a periodical

The recipe now does nothing but assemble. use_embedded_content = True tells calibre the content is already supplied, so it never fetches a page and auto_cleanup never runs. Save as ~/kindle-news/digest.recipe:

๐Ÿ“‹
import json, os
from calibre.web.feeds.news import BasicNewsRecipe

class Digest(BasicNewsRecipe):
    title = 'Weekly Digest'
    language = 'en'
    use_embedded_content = True
    no_stylesheets = True

    def parse_index(self):
        path = os.path.join(os.path.expanduser('~'),
                        'kindle-news', 'digest.json')
        with open(path, encoding='utf-8') as f:
            data = json.load(f)
        return [(s['title'], s['articles']) for s in data['sections']]

It imports only json and os from the standard library, so it runs happily inside calibre's own interpreter without needing anything installed there.

6. The build script

Runs both stages, then copies across if the Kindle happens to be connected. Save as ~/kindle-news/fetch.sh and make it executable:

๐Ÿ“‹
#!/bin/sh
set -e

DIR="$HOME/kindle-news"
out="$DIR/$(date +%F)-weekly.mobi"

"$DIR/venv/bin/python" "$DIR/extract.py"
ebook-convert "$DIR/digest.recipe" "$out" --output-profile kindle

mnt=$(findmnt -rno TARGET -S LABEL=Kindle 2>/dev/null || true)
if [ -n "$mnt" ] && [ -d "$mnt/documents" ]; then
    cp --update=none "$out" "$mnt/documents/"
    sync
    echo "copied to $mnt/documents/"
else
    echo "Kindle not mounted; left at $out"
fi

--output-profile kindle sizes images for a 600x800 screen. The MOBI format is deliberate: calibre marks a recipe build as a periodical, which gives you feed-grouped navigation and per-article read tracking on the device rather than one undifferentiated blob.

A finished issue here is about 515KB for 73 articles across 8 sections, and takes roughly 17 seconds of CPU. Before the extraction rework the same issue was 11MB, because most of it was page furniture.

Use cp --update=none rather than cp -n, which is deprecated and warns. Either way, note it will not replace an issue of the same name already on the device โ€” relevant only if you rebuild twice in one day.

7. Deliver it on plug-in

Two systemd user units remove the manual steps. The first builds the issue on a schedule. Put it in ~/.config/systemd/user/kindle-news.service:

๐Ÿ“‹
[Unit]
Description=Build the weekly Kindle news digest
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=%h/kindle-news/fetch.sh

And its timer, in kindle-news.timer:

๐Ÿ“‹
[Unit]
Description=Build the weekly digest on Saturday morning

[Timer]
OnCalendar=Sat 07:00
Persistent=true
Unit=kindle-news.service

[Install]
WantedBy=timers.target

Persistent=true is the reason to prefer this over cron. If the machine is off on Saturday morning, cron silently skips the run and you get nothing. systemd runs it at the next boot instead.

The second pair copies pending issues once the Kindle appears. In kindle-sync.service, substituting your own mount path from step 2:

๐Ÿ“‹
[Unit]
Description=Copy pending news issues to the Kindle
ConditionPathExists=/media/youruser/Kindle/documents

[Service]
Type=oneshot
ExecStart=/bin/sh -c 'cp -n %h/kindle-news/*.mobi /media/youruser/Kindle/documents/ 2>/dev/null; sync; true'

The ConditionPathExists line is what keeps this quiet. When the Kindle is unplugged the unit is skipped rather than failed, so the journal stays clean all week.

Its timer, in kindle-sync.timer, checks every couple of minutes:

๐Ÿ“‹
[Unit]
Description=Check whether the Kindle is plugged in

[Timer]
OnBootSec=1min
OnUnitActiveSec=2min
Unit=kindle-sync.service

[Install]
WantedBy=timers.target

Enable both:

๐Ÿ“‹
systemctl --user daemon-reload
systemctl --user enable --now kindle-news.timer kindle-sync.timer

A warning from getting this wrong first: do not use a systemd .path unit with PathExists for the sync. The condition stays true after the service finishes, so the unit retriggers itself in a loop until it hits the start limit and fails. A timer cannot do that.

8. Purge old issues without eating your books

Issues accumulate in two places. Deleting by age alone is dangerous, because sideloaded books are frequently older than the issues and would go first.

Anchor the deletion on the filename pattern instead. The build script names every issue with a date prefix, and no book does:

๐Ÿ“‹
find "$DIR" -maxdepth 1 -name "????-??-??-*.mobi" -mtime +14 -delete

Add that as an ExecStartPost line to each of the two services above, pointing at %h/kindle-news in the build unit and at the Kindle's documents directory in the sync unit.

Test it before trusting it. Age a couple of real books artificially and confirm they survive:

๐Ÿ“‹
touch -d '30 days ago' "$KINDLE/documents/some-book.mobi"
find "$KINDLE/documents" -maxdepth 1 -name "????-??-??-*.mobi" -mtime +14 -print

Use -print first and -delete only once the output contains nothing you want to keep. Restore the timestamps afterwards with touch -d.

One detail decides whether this behaves sensibly. cp without -p sets the destination's modification time to the moment of copying, not the build time. The fourteen days therefore run from when an issue reached the device, so an issue you never collected does not arrive already expired.

Deleting an issue from the device leaves its .mbp sidecar behind, which is where the Kindle stores reading position. Remove both together.

9. Get told when it needs plugging in

If you followed self-hosting push notifications, the last gap closes here. This sends a nudge only when an issue is waiting and the Kindle is not connected, because when it is connected the sync timer collects it within two minutes and there is nothing to tell you.

Save as ~/kindle-news/remind.sh:

๐Ÿ“‹
#!/bin/sh
set -e

[ "$1" = "--force" ] || [ -z "$(findmnt -rno TARGET -S LABEL=Kindle 2>/dev/null)" ] || exit 0

. "$HOME/.config/ntfy/notify.env"
: "${NTFY_URL:?set NTFY_URL in ~/.config/ntfy/notify.env}"
: "${NTFY_TOPIC:?set NTFY_TOPIC in ~/.config/ntfy/notify.env}"

pending=$(ls -1 "$HOME"/kindle-news/*.mobi 2>/dev/null | wc -l)

curl -fsS -m 10 \
    -H "Title: Weekly digest ready" \
    -H "Tags: books" -H "Priority: low" \
    -d "New issue built. Plug in the Kindle to collect it ($pending waiting)." \
    "$NTFY_URL/$NTFY_TOPIC" >/dev/null

Add it as a further ExecStartPost on the build service. Keep the URL and topic in ~/.config/ntfy/notify.env at mode 600 rather than in the script, the same as the update script in the ntfy guide does. The topic is a secret, not a label: anyone who knows the string can read and publish to it.

The --force flag exists so you can test the notification without unplugging the device.

10. What doesn't work any more

Everything below was tested on the device and failed. It is here so you do not spend an evening on it.

Wireless delivery of any kind. Amazon states that affected e-readers now support personal documents over USB only. Send to Kindle no longer reaches them. The underlying reason is that the hardware cannot negotiate modern TLS and cannot be patched to do so, which is also why the built-in browser can no longer load most of the web.

Some publications cannot be included at all, and the page will not tell you so. National Review's articles are served as a JavaScript payload. The HTML looks complete โ€” 454KB, an <article> element, the right <title>, and 108 <p> occurrences in the raw text โ€” but the parsed DOM contains zero paragraph nodes, because all 108 sit inside <script> tags, and its structured data carries no articleBody. Every extraction mode returned nothing, as did a direct structural fallback. Only a headless browser can read a site like that, which means several hundred megabytes of Playwright and a browser launch per article for one feed. Count real DOM nodes before blaming your extractor, and be prepared to drop the source.

Jailbreaking to get around it. It does not help here. On the Kindle 4 specifically, Wi-Fi stays active only while a foreground application holds it, so wireless delivery is awkward even with root. Jailbreaking remains worthwhile for other reasons โ€” KOReader brings EPUB, comics and reflowable PDF โ€” but not for this.

KOReader purely to read more books. If you already run calibre, it converts EPUB to MOBI automatically on transfer, so the jailbreak buys convenience rather than capability. Worth it for PDFs and comics; not worth it otherwise.

The device as a dashboard, if you own a tablet. The e-ink dashboard is the best-documented Kindle project there is, and kindle-dash names the Kindle 4 NT as its tested device. It is still a worse dashboard than any tablet you already have on a wall. Its real advantage is sunlight and battery life, which is a reading advantage, not a dashboard one.

11. The script

Steps 3 through 9, automated. Run it on the desktop, as yourself โ€” it needs no root, writes only inside your home directory, and touches nothing on the Kindle except copying issues into its documents folder:

๐Ÿ“‹
curl -O https://nevrast.xyz/kindle.sh
sh kindle.sh

Install calibre first. The script checks for it and stops rather than guessing your package manager.

It pauses where a step needs you: if the Kindle is not plugged in it asks for the mount point, because that path has to be written literally into the systemd unit โ€” ConditionPathExists cannot run a command. Everything else is detected. Read it before running it, the same as anything else you pipe into a shell.

Download it with curl -O and run it as a file rather than piping it into sh. A piped script has the pipe as its standard input, so that one prompt would silently read a line of the script instead of your answer.

It installs four technology feeds as examples, then offers to drop them and asks for your own. Every URL you give it is put through the --check above before it is accepted, and anything unusable is refused with the reason rather than quietly added. You can skip that and edit feeds.txt later; if it is not running on a terminal it skips the questions entirely.

An existing feeds.txt is never overwritten, so re-running the script to update the code will not cost you your feed list.

The ntfy notification is optional and stays off until you create ~/.config/ntfy/notify.env; without that file the build still succeeds.

Source is also on GitHub, as individual files rather than one blob. The single-file version above is generated from them, so the two cannot drift apart.

Email me with questions or fixes.

โ† back home ยท all posts