Fix paywall false positives + add og:image fallbacks

- Body extractor falls back to <article>/<main> container text when
  <p> extraction is sparse, catching <br>-separated bodies (Phoronix).
- Detect Cloudflare bot-block / JS-challenge pages on 403/503 and
  treat them as transport failures rather than paywalls (Brooklyn Vegan).
- og:image extractor falls back to img.wp-post-image and the first
  content <img> in <article>/<main>, with lazy-load placeholder
  handling via data-src / data-lazy-src (Hardcore Gaming 101).
- New -backfill-paywall flag re-checks paywalled=1 rows with the
  current logic, clearing false positives and filling missing thumbs.
This commit is contained in:
prosolis
2026-05-25 12:06:29 -07:00
parent 71dc6d77ab
commit b8dcaa2aa1
2 changed files with 214 additions and 28 deletions

53
main.go
View File

@@ -27,6 +27,7 @@ func main() {
test := flag.Bool("test", false, "post one story to verify the full pipeline, then exit")
testSource := flag.String("test-source", "", "source name to use for -test (default: first enabled)")
local := flag.Bool("local", false, "web/RSS-only mode: poll feeds and serve the web UI, no Matrix login or posting")
backfillPaywall := flag.Bool("backfill-paywall", false, "re-check paywalled rows with current detection logic; clear false positives and fill missing thumbnails, then exit")
flag.Parse()
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
@@ -57,6 +58,11 @@ func main() {
return
}
if *backfillPaywall {
runBackfillPaywall()
return
}
// Test mode: post one story to verify the full pipeline, then exit
if *test {
runTest(cfg, *testSource)
@@ -373,3 +379,50 @@ func runTest(cfg *config.Config, sourceName string) {
func timeNow() int64 {
return time.Now().Unix()
}
// runBackfillPaywall re-runs the current paywall detection logic against
// every row marked paywalled=1. For each row we re-fetch the stored article
// URL; if the new logic no longer considers it gated we clear the flag and,
// when the row has no thumbnail, populate it from the live og:image.
func runBackfillPaywall() {
rows, err := storage.Get().Query(`SELECT guid, article_url, image_url FROM stories WHERE paywalled = 1`)
if err != nil {
slog.Error("backfill: query failed", "err", err)
os.Exit(1)
}
type rec struct{ guid, url, img string }
var pending []rec
for rows.Next() {
var r rec
if err := rows.Scan(&r.guid, &r.url, &r.img); err != nil {
slog.Error("backfill: scan failed", "err", err)
continue
}
pending = append(pending, r)
}
rows.Close()
slog.Info("backfill: starting", "candidates", len(pending))
cleared, imaged, stillGated := 0, 0, 0
for _, r := range pending {
meta := ingestion.FetchArticleMeta(r.url)
gated := meta.Gated() || (meta.Fetched && meta.BodyChars < ingestion.PaywallBodyThreshold)
if gated {
stillGated++
slog.Info("backfill: still gated", "guid", r.guid, "status", meta.Status, "body_chars", meta.BodyChars)
continue
}
newImg := r.img
if newImg == "" && meta.ImageURL != "" {
newImg = meta.ImageURL
imaged++
}
if _, err := storage.Get().Exec(`UPDATE stories SET paywalled = 0, image_url = ? WHERE guid = ?`, newImg, r.guid); err != nil {
slog.Error("backfill: update failed", "guid", r.guid, "err", err)
continue
}
cleared++
slog.Info("backfill: cleared paywall flag", "guid", r.guid, "filled_image", newImg != r.img)
}
slog.Info("backfill complete", "cleared", cleared, "still_gated", stillGated, "thumbnails_filled", imaged)
}