fix(reddit): restore free path via keyless RSS + shreddit scrape (.json is dead) (#457)
* test(reddit): add live RSS + shreddit comment fixtures Captured from reddit.com on 2026-05-29 (search.rss listing + the /svc/shreddit/comments partial), trimmed to a representative subset plus two synthetic edge cases (deleted author, negative score) for offline parser tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(http): add keyless get_text helper Browser-UA text fetch for RSS/HTML endpoints; returns None on any HTTP or network failure so tiered callers fall through cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reddit): keyless RSS discovery (search.rss + listing feeds) Replaces the now-403 search.json with keyless Atom feeds, normalized to the existing reddit_public post shape. Scores are placeholder zeros, backfilled during shreddit enrichment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reddit): keyless shreddit comment scraper Parses <shreddit-comment> elements from /svc/shreddit/comments/r/{sub}/t3_{id} (score/author/created/permalink + thingId-anchored body) into top comments, matching reddit_enrich output. Replaces the dead {thread}.json enrichment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reddit): tiered keyless orchestrator Tier 0 one-shot .json (residential bonus) -> Tier 1 RSS discovery -> Tier 2 shreddit enrichment. Returns [] never raises, so the SC backup still engages when every keyless tier is empty. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reddit): route free path through keyless pipeline (.json is dead) search_reddit_public is now a thin shim over reddit_keyless, so pipeline.py and other callers need no change. Removes the dead .json enrichment helpers; search/_parse_posts remain as the demoted Tier 0 attempt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reddit): request sort=top so true top comments land on page 1 Guarantees the highest-scored comments are captured even on large threads, independent of Reddit's default comment sort. Local score re-sort remains. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reddit): recover post upvote scores via keyless listing partials The shreddit community-more-posts partial server-renders each post's score and comment count (works for normal users, not IP-gated), unlike RSS or the comments endpoint. Use it as a scored discovery source and to backfill scores onto RSS-discovered posts (subreddits derived from results when not provided). Ranking now uses real upvote score. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reddit): listings backfill scores only on bare queries, not discovery Caught running the full pipeline on a bare topic: deriving subreddits from noisy RSS results and merging their top/hot listings flooded results with high-upvote off-topic posts. Now derived-subreddit listings are used only to backfill scores onto keyword-matched RSS posts; listing cards are merged as discovery only when the caller explicitly provides subreddits (on-topic). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -223,6 +223,53 @@ def post_raw(url: str, json_data: Dict[str, Any], headers: Optional[Dict[str, st
|
||||
return request("POST", url, headers=headers, json_data=json_data, raw=True, **kwargs)
|
||||
|
||||
|
||||
BROWSER_USER_AGENT = (
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/124.0.0.0 Safari/537.36"
|
||||
)
|
||||
|
||||
|
||||
def get_text(
|
||||
url: str,
|
||||
timeout: int = DEFAULT_TIMEOUT,
|
||||
retries: int = 2,
|
||||
accept: str = "*/*",
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> Optional[str]:
|
||||
"""Fetch a URL and return decoded text, or None on any failure.
|
||||
|
||||
Keyless helper for Reddit RSS and shreddit HTML endpoints — the free path
|
||||
that replaced the now-403 ``.json`` endpoints. Sends a browser User-Agent
|
||||
and never raises: returns None on HTTP error, network failure, or timeout
|
||||
so tiered callers can fall through to the next source.
|
||||
|
||||
Args:
|
||||
url: Request URL
|
||||
timeout: HTTP timeout per attempt in seconds
|
||||
retries: Number of retries on failure (kept low — these tiers fail fast)
|
||||
accept: Accept header value (e.g. "application/atom+xml", "text/html")
|
||||
headers: Optional extra headers merged over the defaults
|
||||
|
||||
Returns:
|
||||
Decoded response body as text, or None on failure.
|
||||
"""
|
||||
merged = {
|
||||
"User-Agent": BROWSER_USER_AGENT,
|
||||
"Accept": accept,
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
}
|
||||
if headers:
|
||||
merged.update(headers)
|
||||
try:
|
||||
return request(
|
||||
"GET", url, headers=merged, timeout=timeout, retries=retries, raw=True
|
||||
)
|
||||
except HTTPError as e:
|
||||
log(f"get_text failed ({e}): {url}")
|
||||
return None
|
||||
|
||||
|
||||
def scrapecreators_headers(token: str) -> Dict[str, str]:
|
||||
"""Build ScrapeCreators request headers (x-api-key + JSON content type)."""
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user