Read-time: full-article "Full story · ~N min" badge (Option B)
Replaces the gist-based read-time with the SOURCE article's full read time — the
contrast that sells the gist ("calm 1-min version here; ~10 min for the deep dive").
- goodnews/readtime.py: word_count_from_html (strips script/style/nav/header/
footer/form/button/aside furniture before counting) + source_read_minutes
(~225 wpm, 200-word floor, None when extraction looks failed/too thin).
- articles.source_words + read_checked_at columns (count only, never the body;
fits the privacy posture). Idempotent migration.
- enrich.fetch_source_words + enrich_read_times: a bounded, retry-guarded cycle
step (mirrors the image enrichers) that counts words for recent accepted
articles. Only ever writes a real count; never overwrites good with zero. Wired
into the cycle after recent-image enrichment.
- queries: source_words flows through _ARTICLE_COLUMNS; api exposes
source_read_minutes on Article (null when unknown).
- home3: News card shows "Full story · ~N min", hidden entirely when null (no
misleading "1 min").
- Tests: furniture stripping, threshold/rounding, enrich idempotency + no
zero-overwrite, API null handling. 412 backend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,8 @@ CREATE TABLE IF NOT EXISTS articles (
|
||||
title_hash TEXT,
|
||||
duplicate_of INTEGER REFERENCES articles(id) ON DELETE SET NULL,
|
||||
image_checked_at TEXT,
|
||||
source_words INTEGER, -- full-article word count (metadata only; never the body)
|
||||
read_checked_at TEXT, -- when we last tried to count words (retry guard)
|
||||
FOREIGN KEY (source_id) REFERENCES sources(id)
|
||||
);
|
||||
|
||||
@@ -595,6 +597,10 @@ def _migrate(conn: sqlite3.Connection) -> None:
|
||||
)
|
||||
if "image_checked_at" not in article_cols:
|
||||
conn.execute("ALTER TABLE articles ADD COLUMN image_checked_at TEXT")
|
||||
if "source_words" not in article_cols: # full-article read-time (count only, no body)
|
||||
conn.execute("ALTER TABLE articles ADD COLUMN source_words INTEGER")
|
||||
if "read_checked_at" not in article_cols:
|
||||
conn.execute("ALTER TABLE articles ADD COLUMN read_checked_at TEXT")
|
||||
# Created here (not in SCHEMA) so it runs after the column exists on upgrades.
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_articles_duplicate_of ON articles(duplicate_of)")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user