From 4e24c45f6cf28722fcd30bc0431d4fb586fa719c Mon Sep 17 00:00:00 2001 From: jamie-at-bunny Date: Thu, 15 Oct 2026 15:00:05 +0100 Subject: [PATCH 1/2] fix(stream): separate source-missing import failures and name the import list cap --- .changeset/stream-import-missing-source.md | 6 ++++++ packages/cli/src/commands/stream/import/list.ts | 4 +++- packages/cli/src/commands/stream/import/run.ts | 9 +++++++-- packages/stream-import/src/constants.ts | 3 +++ packages/stream-import/src/migration.ts | 12 +++++++++--- packages/tools/src/stream/import/index.ts | 7 +++++-- packages/tools/src/stream/import/model.ts | 2 ++ 7 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 .changeset/stream-import-missing-source.md diff --git a/.changeset/stream-import-missing-source.md b/.changeset/stream-import-missing-source.md new file mode 100644 index 00000000..6b3d70a2 --- /dev/null +++ b/.changeset/stream-import-missing-source.md @@ -0,0 +1,6 @@ +--- +"@bunny.net/cli": patch +"@bunny.net/stream-import": patch +--- + +fix(stream): stop suggesting --clean-failed alone for videos the source no longer lists, and name the 10,000-video cap in `stream import list` diff --git a/packages/cli/src/commands/stream/import/list.ts b/packages/cli/src/commands/stream/import/list.ts index 183efd31..6c5b03b3 100644 --- a/packages/cli/src/commands/stream/import/list.ts +++ b/packages/cli/src/commands/stream/import/list.ts @@ -41,7 +41,9 @@ function renderListing(listing: ImportListing, output: OutputFormat): void { ), ); if (listing.truncated) - logger.dim(`Showing the first ${listing.videos.length} videos.`); + logger.dim( + `Showing the first ${listing.videos.length.toLocaleString("en-US")} videos, the most one listing returns. Narrow it with --folder (or --prefix for S3).`, + ); return; } if (!listing.supportsFolders) { diff --git a/packages/cli/src/commands/stream/import/run.ts b/packages/cli/src/commands/stream/import/run.ts index df44d3f8..ebb69d55 100644 --- a/packages/cli/src/commands/stream/import/run.ts +++ b/packages/cli/src/commands/stream/import/run.ts @@ -255,12 +255,17 @@ function renderResult( logger.log(""); for (const m of failed) logger.error(`${stripAnsi(m.name)}: ${m.error}`); } - const givenUp = failed.filter((f) => !f.retryable).length; + const missing = failed.filter((f) => f.missingAtSource).length; + const givenUp = failed.filter((f) => !f.retryable).length - missing; if (givenUp > 0) logger.info( `${givenUp} cannot be retried as they are: Bunny rejected them or they failed 3 times. Add --clean-failed to delete their copies and try again.`, ); - if (failed.length > givenUp || result.status === "paused") + if (missing > 0) + logger.info( + `${missing} are no longer listed at the source. Restore them there, then add --clean-failed to try again.`, + ); + if (failed.length > givenUp + missing || result.status === "paused") logger.info( `Resume with ${bunny(resumeCommand(result, libraryArg, includeTs))}`, ); diff --git a/packages/stream-import/src/constants.ts b/packages/stream-import/src/constants.ts index 0fb59c2d..730e8960 100644 --- a/packages/stream-import/src/constants.ts +++ b/packages/stream-import/src/constants.ts @@ -18,6 +18,9 @@ export const MAX_VIDEO_SHORT_EDGE = 2_160; /** A resume stops retrying a video after this many fetches; a fresh run or `cleanFailed` tries it again. */ export const MAX_IMPORT_ATTEMPTS = 3; +/** The error on an entry the source stopped listing; only restoring it there lets a retry succeed. */ +export const SOURCE_MISSING_ERROR = "No longer listed at the source"; + export const VIDEO_EXTENSIONS = [ ".mp4", ".mov", diff --git a/packages/stream-import/src/migration.ts b/packages/stream-import/src/migration.ts index 2cb80151..9122e328 100644 --- a/packages/stream-import/src/migration.ts +++ b/packages/stream-import/src/migration.ts @@ -16,6 +16,7 @@ import { MAX_VIDEO_DURATION_SECONDS, MAX_VIDEO_LONG_EDGE, MAX_VIDEO_SHORT_EDGE, + SOURCE_MISSING_ERROR, } from "./constants.ts"; import type { Logger, @@ -429,7 +430,7 @@ export class MigrationService { (!folderId || m.sourceFolderId === folderId); if (!gone || signal?.aborted) continue; m.status = "failed"; - m.error = "No longer listed at the source"; + m.error = SOURCE_MISSING_ERROR; m.permanent = true; } state.status = signal?.aborted @@ -604,9 +605,14 @@ export class MigrationService { (m) => m.status === "failed" && inScope(m), ); const givenUp = failed.filter((m) => this.givenUp(m)); - if (givenUp.length > 0) + const missing = givenUp.filter((m) => m.error === SOURCE_MISSING_ERROR); + if (givenUp.length > missing.length) this.logger.warn( - `Not retrying ${givenUp.length} videos that cannot succeed or failed ${MAX_IMPORT_ATTEMPTS} times; a fresh import or \`cleanFailed\` tries them again.`, + `Not retrying ${givenUp.length - missing.length} videos that cannot succeed or failed ${MAX_IMPORT_ATTEMPTS} times; a fresh import or \`cleanFailed\` tries them again.`, + ); + if (missing.length > 0) + this.logger.warn( + `Not retrying ${missing.length} videos the source no longer lists; restore them there, then a fresh import or \`cleanFailed\` tries them again.`, ); const retry = failed.filter((m) => !this.givenUp(m)); if (retry.length === 0) return; diff --git a/packages/tools/src/stream/import/index.ts b/packages/tools/src/stream/import/index.ts index a2117a28..973e4c98 100644 --- a/packages/tools/src/stream/import/index.ts +++ b/packages/tools/src/stream/import/index.ts @@ -17,6 +17,7 @@ import { type MigrationState, readMigrationState, refreshMigrationState, + SOURCE_MISSING_ERROR, videoStatusText, } from "@bunny.net/stream-import"; import { z } from "zod"; @@ -480,9 +481,11 @@ export const streamImportRun = defineTool({ name: m.videoName, sourceId: m.sourceVideoId, error: m.error, + missingAtSource: m.error === SOURCE_MISSING_ERROR, retryable: - Boolean(input.cleanFailed) || - (!m.permanent && (m.attempts ?? 0) < MAX_IMPORT_ATTEMPTS), + m.error !== SOURCE_MISSING_ERROR && + (Boolean(input.cleanFailed) || + (!m.permanent && (m.attempts ?? 0) < MAX_IMPORT_ATTEMPTS)), })), collections: state.folderMappings, warnings, diff --git a/packages/tools/src/stream/import/model.ts b/packages/tools/src/stream/import/model.ts index 4207f8df..f14b453b 100644 --- a/packages/tools/src/stream/import/model.ts +++ b/packages/tools/src/stream/import/model.ts @@ -87,6 +87,8 @@ export const ImportRunSchema = z.object({ name: z.string(), sourceId: z.string(), error: z.string().nullable(), + /** The source no longer lists it, so only restoring it there can bring it back. */ + missingAtSource: z.boolean(), /** False when a resume will not retry it: a retry cannot fix it, or it has failed too often. */ retryable: z.boolean(), }), From 0d1f996229dab7a954e20c486f110d5957ee5c4e Mon Sep 17 00:00:00 2001 From: jamie-at-bunny Date: Thu, 15 Oct 2026 15:05:22 +0000 Subject: [PATCH 2/2] fix(stream): only suggest narrowing flags the source supports --- packages/cli/src/commands/stream/import/list.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/stream/import/list.ts b/packages/cli/src/commands/stream/import/list.ts index 6c5b03b3..48ee04dc 100644 --- a/packages/cli/src/commands/stream/import/list.ts +++ b/packages/cli/src/commands/stream/import/list.ts @@ -40,10 +40,18 @@ function renderListing(listing: ImportListing, output: OutputFormat): void { output, ), ); - if (listing.truncated) + if (listing.truncated) { + // Only suggest a flag the source accepts: flat sources reject --folder. + const narrow = + listing.source === "s3" + ? " Narrow it with --prefix." + : listing.supportsFolders + ? " Narrow it with --folder." + : ""; logger.dim( - `Showing the first ${listing.videos.length.toLocaleString("en-US")} videos, the most one listing returns. Narrow it with --folder (or --prefix for S3).`, + `Showing the first ${listing.videos.length.toLocaleString("en-US")} videos, the most one listing returns.${narrow}`, ); + } return; } if (!listing.supportsFolders) {