FeedBulkImporter
\Benecaster\Feed\FeedBulkImporter
The one-time episode import: every item in a podcast RSS feed becomes a draft episode on a show, in a background WP-Cron job with a progress counter. It is what the setup wizard’s Import step runs, through POST /shows/{id}/import. A stable core API (operator ruling 2026-09-15: episode import is core, and the Migration Wizard add-on surfaces it rather than owning a copy). Free, and not licence-gated.
Not Feed Sync. Ongoing sync of new episodes is Feed\FeedImporter (Show Settings → Feed Sync, the benecaster_feed_sync_* hooks). The two share no code path: this class never fires a benecaster_feed_sync_* hook, and FeedImporter never fires benecaster_bulk_import_*.
Reaching it. There is no public function wrapper and no boot hook of its own — take it from the container that benecaster_boot hands every add-on: $importer = $container->make( \Benecaster\Feed\FeedBulkImporter::class );.
A completed job also sets the show up for Feed Sync — its feed URL becomes the show’s Source feed URL (ShowMeta::SOURCE_FEED_URL, _benecaster_show_source_feed_url) when the show has none. This runs for every caller of start_import() alike — the setup wizard, the REST endpoint, and add-on code such as the Migration Wizard’s import step (operator ruling 2026-09-15: an add-on writes core’s setting, never keeps its own copy) — because the adoption happens inside process_import(), not in the wizard or the REST controller. See adopt_source_feed_url() below for the exact rules. Recipe: import-a-back-catalogue-from-an-addon.
Methods
| Method | Visibility | Since | Description |
|---|---|---|---|
start_import( int $show_id, string $feed_url ): string |
Public | — | Records the job, sets it as the show's active job, schedules benecaster_run_bulk_import as a single WP-Cron event for now, and returns an opaque job ID (16 hex characters). It doesn't fetch anything, validate the URL, or check that the show exists — the caller validates, as ImportController does (404 for a non-show, 400 invalid_feed_url for a URL FILTER_VALIDATE_URL rejects). Starting a second job for the same show replaces the active pointer; the earlier job's progress stays readable by its own ID. |
get_progress( string $job_id ): ?array |
Public | — | Returns the job's current progress: total (items in the feed, 0 until the feed is parsed), imported, skipped (GUID already on the show), errors (draft insert failed) — all int — status ('running', 'complete' or 'failed'), and error_msg (only when status is 'failed'; translated: "Could not fetch feed URL." or "Feed is not valid RSS."). Returns null if the job ID is unknown. This is what the REST poll endpoint and the wizard's React polling loop both read; counts update after every item, so polling shows live progress. |
get_active_job_id( int $show_id ): string |
Public | — | The show's most recently started job ID, or an empty string if none has ever been started. It isn't cleared when the job ends, so read get_progress() for the current state. |
process_import( string $job_id ): void |
Public | — | The WP-Cron callback. Don't call it yourself — it's public only because WP-Cron needs it; calling it runs the whole import synchronously in the current request. Fetches the feed (30-second timeout), fires benecaster_bulk_import_started before the fetch, and creates one draft benecaster_episode per item whose GUID isn't already on the show. Maps title, ` → body, / → RSS description, , enclosure URL/length/ type, and iTunes duration, episode, season, episode type, explicit, author and image. Also stores the item's as _benecaster_source_link (EpisodeMeta::SOURCE_LINK), with the same key and validation as Feed Sync (EpisodeMeta::sanitize_source_link()) — unset when the item has no or it isn't an http(s) URL with a host — written before benecaster_episode_imported fires. Fires benecaster_episode_imported per draft. A fetch failure or non-RSS response marks the job 'failed' and returns before any Source-feed-URL adoption runs. On success, calls adopt_source_feed_url() before marking the job 'complete'. Every path — failure or success — then fires benecaster_bulk_import_completed via the private finish()` helper. |
adopt_source_feed_url( int $show_id, string $feed_url ): void |
— | — | Saves the imported feed as the show's Source feed URL (Show Settings → Feed Sync) — only on a completed import (a failed fetch or non-RSS response saves nothing, so a mistyped URL is never adopted, and is never blocked on a retry with the corrected one), and only when the show has no Source feed URL already (an existing value — a re-run wizard, an import started after setup — is the podcaster's and is kept, even when it differs from the newly imported feed). Never writes the sync mode — _benecaster_show_sync_mode stays at its default, Manual, per the operator's 2026-09-15 ruling that nothing runs in the background the podcaster didn't choose. Not a supported extension point; documented here because it is the side effect every caller of start_import() inherits. Runs before finish(), so a benecaster_bulk_import_completed callback already sees the saved URL. |
Constants
| Name | Value | Description |
|---|---|---|
CRON_HOOK |
'benecaster_run_bulk_import' |
The WP-Cron hook name process_import() is registered against. |
Hooks Fired
Notes
Only the completed-job path writes the Source feed URL. A failed job (unreachable URL, invalid RSS) writes nothing at all — not even a partial progress adoption — so a podcaster who mistypes a feed URL and corrects it on a second attempt never has the first, wrong URL adopted.
The sync mode is deliberately untouched. Only the Source feed URL field is written; _benecaster_show_sync_mode
is never set by this class, so Feed Sync stays on Manual until the podcaster explicitly picks a trigger in
Show Settings → Feed Sync.
One tracked job per show, and no lock. Nothing stops two jobs for one show running at once.
Deduplication is by GUID, read once when each run starts, so two overlapping runs over the same feed can
both create a draft for the same item. Start a new job only after the active one's status is no longer
'running'.
Storage (non-autoloaded wp_options): _benecaster_import_job_{job_id} (show_id, feed_url),
_benecaster_import_progress_{job_id} (the get_progress() shape), _benecaster_import_active_job_{show_id}.
Internal — read them through the methods, not directly. Nothing deletes them yet, not even at uninstall:
Uninstaller::delete_options() matches benecaster_%, which a leading underscore doesn't match (the same
gap affects BulkEnrollmentJobRunner's _benecaster_enroll_*). Filed as a separate fix, 2026-09-15.
REST, for JS callers: POST /shows/{id}/import and GET /shows/{id}/import — same keys as
start_import() / get_progress().
Recipes: import-a-back-catalogue-from-an-addon, email-the-podcaster-when-an-episode-import-finishes.