diff --git a/README.md b/README.md index 2b4b972..187a45a 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,9 @@ or write a daily briefing. It's meant to replace the n8n → Discord workflow. - **Paid articles and ads hidden**: heise+ and Golem `(g+)` are recognized by their title; SPIEGEL+ and t3n Pro by a marker in the article page, which the app checks once per new article in the background (it reads only until the marker, - and remembers results for 14 days). Sponsored posts (`Anzeige:`) are hidden too. + and remembers results for 14 days). Paid-subscriber posts from Substack newsletters + (Interconnects, Ahead of AI, …) are recognized in the feed itself, where Substack + cuts them off with a "Read more" link. Sponsored posts (`Anzeige:`) are hidden too. The feed header shows how many were hidden; both filters can be turned off. - **Ad and cookie-banner blocking** in the original-page view: requests to ~80 ad/tracker hosts and the common consent managers (Sourcepoint, OneTrust, diff --git a/lib/services/paywall_service.dart b/lib/services/paywall_service.dart index c4d9e7d..fa0e1f0 100644 --- a/lib/services/paywall_service.dart +++ b/lib/services/paywall_service.dart @@ -9,6 +9,8 @@ import 'storage.dart'; /// Recognizes paid and sponsored articles so the feed can hide them. /// /// Most paid articles are marked in the title (heise+, Golem's "(g+)"). +/// Substack newsletters cut paid posts short in the feed and end them with a +/// "Read more" link to the post itself, so those are recognized from the feed. /// SPIEGEL and t3n only mark them in the page's schema.org data /// (`"isAccessibleForFree": false`), so for those sources each new article is /// checked once in the background and the result is remembered. @@ -33,6 +35,14 @@ class PaywallService { caseSensitive: false, ); + /// What Substack appends to a paid post's feed content in place of the rest: + /// `
`, as the very end. Free posts + /// end with their own text, even when they ask readers to subscribe. + static final _substackCut = RegExp( + r'\s*\s*Read more\s*\s*
\s*$', + caseSensitive: false, + ); + static final _marker = RegExp(r'''"isAccessibleForFree"\s*:\s*"?(true|false)"?''', caseSensitive: false); @@ -41,7 +51,11 @@ class PaywallService { static const _concurrency = 6; static const _keepFor = Duration(days: 14); - bool isPaid(Article a) => paidTitle.hasMatch(a.title) || (_checks[a.link]?.paid ?? false); + bool isPaid(Article a) => + paidTitle.hasMatch(a.title) || _isCutSubstackPost(a) || (_checks[a.link]?.paid ?? false); + + static bool _isCutSubstackPost(Article a) => + _substackCut.firstMatch(a.contentHtml)?.group(1) == a.link; bool isSponsored(Article a) => sponsoredTitle.hasMatch(a.title); diff --git a/test/fixtures/ahead_of_ai_feed.xml b/test/fixtures/ahead_of_ai_feed.xml new file mode 100644 index 0000000..deee5a3 --- /dev/null +++ b/test/fixtures/ahead_of_ai_feed.xml @@ -0,0 +1,15 @@ +For this, we are going to fine-tune a DistilBERT classifier (similar to what I described in one of my early Substack articles, Finetuning Large Language Models), but more details on that later when we get to that stage.
++ + Read more + +
+ ]]>If you liked my previous Build a Large Language Model (From Scratch) book, this is essentially a sequel implementing inference-time scaling techniques and reinforcement learning algorithms from scratch.
And if you want to support future long-form articles like this one, consider becoming a paid subscriber. It helps me keep writing these independent deep dives and sharing the accompanying code, figures, and experiments.
]]>I’m also hosting a book launch where you can get a free signed copy tomorrow from 5-8PM in Seattle (Fremont/Ballard area) – we still have some extra space so I’m opening signups to paid subscribers below the paywall:
++ + Read more + +
+ ]]>Many AI optimists tend to compare what is happening in this AI boom to the industrial revolution, or to other periods of rapid technological advancement and diffusion into society.
]]>