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 Benecaster API (episode import is built in, and the Migration Wizard add-on surfaces it rather than owning a copy). Free, and not license-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 job is a chain of time-boxed WP-Cron runs, not one run, so a host that stops a run does not end the import. Each run fetches the feed again and starts episodes for about 20 seconds, counted from before the fetch, then queues the next. Each also queues a follow-up about three minutes ahead before it does any work, and that follow-up is what picks up a killed run. A per-show lock stops two runs overlapping. The show’s stored GUIDs are the position, so nothing is recounted. A later fetch that fails is tried up to three times in a row before the job fails; the first run’s fetch fails at once, since a mistyped URL should say so. An episode in flight at a kill keeps what it had, and benecaster_episode_imported may not fire for it.
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 (an add-on writes Benecaster’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. A start for the feed the show's job is already importing hands that job back, queueing it again if nothing is queued. A start for a different feed is not refused: the new job becomes the active one, and the per-show lock keeps the two from overlapping. |
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. Adds stalled: true only while the job is 'running' with nothing written for five minutes, and continuing: true only while its last run came from resume_import(). A Migration Wizard step that calls the resume route can keep calling it while continuing is true. |
resume_import( int $show_id ): void |
Public | — | Resume: runs one slice of the show's job, about 20 seconds, in the current request, because a job usually stalls when WP-Cron is not firing. Does nothing unless the job is stalled or continuing, so a second click cannot start a slice beside a healthy run. The first cron event to run the job ends continuing, which hands the job back to WP-Cron. A running job whose inputs are gone is marked 'failed'. |
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 one slice of about 20 seconds and queues the next. Across its runs, a job fetches the feed (30-second timeout), fires benecaster_bulk_import_started once before the first 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 with the same validation as Feed Sync — 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. When every item is done, 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, so nothing runs in the background that 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. |
RUN_SECONDS |
20 |
How long one run keeps starting episodes, counted from before the fetch. |
STALL_SECONDS |
300 |
Seconds with no progress write before a running job reads as stalled. |
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 a per-show lock. The lock stops two runs for one show overlapping. A start
for the feed the show's job is already importing hands that job back. A start for a different feed is not
refused: the new job becomes the active one, and the earlier job is no longer reachable through
get_active_job_id(). To import a different feed, wait until the active job'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, not even at uninstall:
Uninstaller::delete_options() matches benecaster_%, which a leading underscore doesn't match (the same
gap affects BulkEnrollmentJobRunner's _benecaster_enroll_*).
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.