Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6.6 KiB
6.6 KiB
ripunzel
Node.js CLI that archives reddit users' posts (and direct links to supported media hosts) to disk, with configurable filename patterns and a per-user YAML index to avoid redownloads.
Branches
masteris maintained by hand. Do not commit to or merge intomasterunless explicitly asked.public-reddit-apiwas largely written with Claude Code to get the app working again after reddit revoked API keys (Snoowrap/OAuth no longer usable). It needs a thorough manual review and a major version bump before it may be merged intomaster(see commit2e4ba27).
Running
- Node
>=24.19.0(.nvmrc).npm install, thennpm start -- --user <name>(seesrc/cli.jsor README for all flags). - ffmpeg must be on the PATH to mux v.redd.it video+audio; exiftool ships via
dist-exiftool. - No test suite. Lint with
npx eslint src(airbnb-base, 4-space indent, CommonJS,'use strict'). - Config uses the
configpackage:config/default.jsis committed;config/local.jsoverrides it and is gitignored, as isconfig/cookies.js(a Netscape cookie-jar dump, exported as a string and pulled in bylocal.js). Never print, commit or log cookie contents. output/,logs/,users*,ignoreare local data and gitignored.
Architecture
The pipeline runs from src/app.js:
- Sources (
src/sources/):getUserPosts/getPostsfetch the user profile, social links and submissions through the reddit client, plus the existing index (getIndex, YAML atlibrary.index.file). - Curate (
src/curate/):posts.jsnormalizes reddit post objects, applies filters (index, dates, limit, ignore, include/exclude, duplicates) and runsdissectLinkto choose a host method.user.jsnormalizes the profile. - Info (
src/fetch/info.js): callsmethods[host.method](insrc/methods/) to resolve each post into{ album, items[] }. If that fails, it falls back to reddit preview images (redditPreview). Some methods export{ fetchInfo, fetchPredata }; predata (e.g. a RedGifs temporary token) is fetched once per run. - Content (
src/fetch/content.js): downloads items (src/fetch/item.js, using bhttp), dedupes by blake2 hash, saves to a path built bysrc/interpolate.jsfromconfig.librarypatterns, muxes video+audio, writes EXIF and finally updates the index (src/save/writeToIndex.js).
Reddit client (src/reddit/client.js), the core of this branch
- Calls reddit's public
.jsonendpoints onwww.reddit.com(/user/x/about.json,/user/x/submitted.json,/comments/id.json,/api/morechildren.json) withraw_json=1. There is no OAuth. - Anonymous requests run into reddit's bot challenge, so a logged-in session's cookies (
config.reddit.cookies, a cookie-jar string, orconfig.reddit.cookie, a raw header) are sent. If reddit returns HTML instead of JSON, the cookies have most likely expired. The client warns when the soonest cookie expires within 48h. - Social links have no public JSON endpoint (the widgets API needs OAuth). They are scraped from the profile page HTML (
data-faceplate-tracking-contextattributes), which is fragile and breaks whenever reddit changes its markup. - All reddit requests go through one Bottleneck limiter:
maxConcurrent: 1, reservoir 6/min,minTime1500ms (config.reddit.throttle). This is deliberately slow, because being flagged as a bot defeats the purpose. Retries for 429/503 and network errors happen outsidelimiter.schedule(). Retrying inside it previously caused a deadlock; keep it that way. - Avoid unnecessary reddit calls: gallery posts reuse
media_metadata/gallery_datafrom the submission listing (curate/posts.js→methods/redditAlbum.js).getSubmission(post only, cheap) is separate fromgetSubmissionWithComments(full comment tree includingmorestubs, which is expensive and only used for self posts).
Networking (src/proxy.js, src/limiter.js)
proxy.jsexports a wrappedfetch(undici; per-host proxy fromconfig.proxy.hostswith suffix matching, plus a hardconfig.fetch.timeout) andagentFor()for bhttp. Both cover the reddit API whenreddit.comis configured. fetch() supports http(s) and SOCKS5 (undici's experimentalSocks5ProxyAgent;socks5hmaps to it and hostnames are resolved on the proxy). SOCKS4 throws for fetch() rather than going out directly; bhttp supports all SOCKS variants.config.proxy.retryFlippedretries a failed request via the opposite route (proxied ↔ direct). It is off by default for privacy reasons.limiter.jsprovides per-host Bottleneck limiters for media downloads only (config.limiter.hosts). It is separate from the reddit API limiter.- bhttp has no timeout of its own.
responseTimeoutinfetch/item.jsmakes it abort the socket. Don't replace this with aPromise.race, which would leak connections and eventually freeze downloads.
Fetching strategy
- By default, the entire submission listing is paged (up to reddit's ~1000-post cap). Every post not yet in the index is downloaded, so gaps anywhere in the archive are backfilled.
- With
--after <date>or--after-indexed oldest|latest(sortnewonly),getSubmissionsstops paging at the first page that crosses the cutoff, ignoring pinned posts.--after-indexed latestis the cheap "only fetch what's new" mode, and it does not backfill gaps. - The user, then the index, then the posts are fetched in that order, because the index decides the cutoff.
- Media data for videos and galleries comes from the listing, including crossposts via
crosspost_parent_list[0]. A post is only fetched again if that data is missing.
Known caveats / review notes
- Posts beyond reddit's 1000-post listing cap can't be reached through reddit. Arctic Shift (
arctic-shift.photon-reddit.com/api/posts/search?author=…) returns full reddit post objects and is the candidate replacement for the abandoned--archivescode (src/archives/, whose sites are gone). getUserPostsprocesses users with concurrency 10, but the reddit client is serial anyway.- Self posts fetch their full comment tree, which takes at least one request per post (more with
morestubs). - gfycat (shut down in 2023) links are routed to the RedGifs method under the same ID, keeping the label
gfycat. eroshare is recovered from a fixed Wayback Machine snapshot. vidble is still online. - The README's "API keys" section and date-format notes predate this branch.
- Several files have long explanatory comments from the rewrite. Keep them accurate when changing behavior.
- No test suite. To test reddit logic offline, stub
require('./src/proxy').fetchbefore requiring the client, and raisereddit.throttleviaNODE_CONFIG, or the 6/min limiter applies.