Substack import: posts and subscribers (#24 phase 4) - #245
Merged
Merged
Conversation
Admins upload Substack's export zip (or a bare email list CSV) at /admin/imports. Posts import from posts.csv and posts/*.html with published/draft status and paid/free audience mapped to visibility; Substack embeds, buttons, paywall markers and CDN images are converted. Subscribers are created directly as confirmed with their original signup date, without confirmation emails or webhooks; email-disabled rows import as unsubscribed and paid/comp subscribers are labelled. The zip is read in memory with entry-count and decompressed-size caps. Adds the csv gem, which Ruby 3.4 no longer ships as a default gem. Closes #24 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wrap each post in import_item, and give each subscriber row its own savepoint so an unexpected failure rolls back that row instead of the surrounding batch of 500. Labels are resolved before the savepoint so a rolled-back row can't invalidate a memoized label. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner
Author
|
Merged the stack up to date. Added per-item error isolation for Substack in |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 4, the final phase of #24: admins can upload Substack's export .zip (Settings → Exports) to import posts and the subscriber list, or upload just the email list
.csv. Post content comes from the export itself, so there's no need to scrape the live Substack site.Changes
Reading the export
Imports::Substack::ExportReader:posts.csv,posts/<post_id>.htmlandemail_list*.csv, matched by path suffix so a wrapping folder still works.InvalidFile.Posts
Imports::Substack::ContentConverter(on the shared converter base):data-attrs: YouTube, Vimeo, tweets, embedded posts, Spotify and SoundCloud become links; pullquotes become blockquotes.substackcdn.com/image/fetch/<transforms>/<url>through the SSRF-guarded downloader.Imports::SubstackImporter:post_id(123.slug)is_published+post_dateeveryone/only_free/only_paid&foundingpublic/members_only/paid_onlySubscribers
Subscriber.new, notsubscribe_or_sign_in!, so no confirmation emails and nosubscriber.createdwebhooks.confirmed_at/created_atset to the Substack signup date (growth charts keep their history).email_disabledrows become unsubscribed.Other
Import:substacksource;ACCEPTED_FILESnow allows several extensions per source. The import row shows subscriber counts.csvgem, which Ruby 3.4 no longer ships as a default gem (without it:LoadErrorat runtime).Documentation
csvgem, the converter, the test zip helper)Testing
data-attrs, Vimeo), importer (post mapping, skips, subscriber confirmation/unsubscribe/dates/labels, existing subscribers untouched, bare CSV, no mail/webhooks/notifications, idempotency), model, job, controllerOut of scope: Ghost members CSV (small follow-up reusing this subscriber logic), Substack open/click stats CSVs, podcast audio, featured images (not included in Substack's export).
Closes #24
🤖 Generated with Claude Code