commit b8fe7c2c2baf6fe48619f88bc92b83ef5bd745e2
parent 470b78d696e83fe5b9a3eff907f5fdcf0d0b4b94
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 22 Sep 2026 16:04:23 -0400
editor: Sync all skips a channel whose media drive is not mounted
Every enumerator of `data/` swallows ENOENT as "this channel has no videos", so
a sync of a channel on an unmounted platter reads the whole back catalogue as
undownloaded and hands the download lane an instruction to re-fetch hundreds of
gigabytes onto the volume that was too full to hold them.
`inspectChannelMedia` is the one module that can tell those two apart, and
`queueForSlugs.skip` already accepted a promise. ok and in-place are the healthy
answers; everything else is a skip carrying the inspector's own sentence, so the
readout names the drive instead of reporting a clean sweep over a channel
nothing could read.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
2 files changed, 62 insertions(+), 1 deletion(-)
diff --git a/editor/app/channels/actions.ts b/editor/app/channels/actions.ts
@@ -21,6 +21,7 @@ import {
writeChannelConfig,
} from "yt-dlp-transcript-common/controller/channels";
import { renameChannel } from "yt-dlp-transcript-common/controller/renameChannel";
+import { inspectChannelMedia } from "yt-dlp-transcript-common/lib/channelMedia";
import { channelMediaBusyReason } from "./lib/mediaBusy";
import {
excludedDownloadIdSet,
@@ -546,11 +547,31 @@ export async function syncAllChannelsAction(
a.localeCompare(b),
);
const outcome = await queueForSlugs(order, {
- skip: (slug) => {
+ // AN UNMOUNTED DRIVE IS NOT AN EMPTY CHANNEL, and a sync is exactly the job
+ // that acts on that mistake. Every enumerator of `data/` swallows ENOENT as
+ // "no videos" (AGENTS.md), so a sync of a channel whose platter is not
+ // there reads the whole back catalogue as undownloaded and hands the
+ // download lane an instruction to re-fetch hundreds of gigabytes onto the
+ // volume that was too full to hold them.
+ //
+ // `inspectChannelMedia` is the one module that can tell the two apart. Two
+ // stats and at most one small JSON read per channel — cheap enough for a
+ // 68-channel pool, and the reason `queueForSlugs.skip` may be async.
+ //
+ // ok and in-place are the two healthy answers: the media is where config
+ // says, or there is no relocation at all. Everything else — `unreachable`,
+ // `in-transition`, `inconsistent` — is a skip with the inspector's own
+ // sentence, so the bulk bar names the drive rather than reporting a
+ // successful sweep over a channel nothing could read.
+ skip: async (slug) => {
const config = bySlug.get(slug);
if (!config?.url) return "no url";
if (isChannelPaused(priority, slug, "sync")) return "paused for sync";
if (active.has(slug)) return "already running";
+ const media = await inspectChannelMedia(paths, slug, config);
+ if (media.status !== "ok" && media.status !== "in-place") {
+ return `media ${media.status}: ${media.detail ?? "not reachable"}`;
+ }
return null;
},
run: (slug) => syncAction(slug, undefined, opts?.fullSweep),
diff --git a/editor/e2e/channel-storage.spec.ts b/editor/e2e/channel-storage.spec.ts
@@ -3,6 +3,7 @@ import {
lstat,
mkdir,
readdir,
+ rm,
symlink,
utimes,
writeFile,
@@ -766,3 +767,42 @@ test("the Storage panel evicts this channel's old clip windows", async ({
// a queued job, not part of the eviction.
await expect.poll(clipsBytesOf, { timeout: 60_000 }).toBeLessThan(before);
});
+
+// SYNC ALL SKIPS A CHANNEL WHOSE DRIVE IS NOT THERE, and says which.
+//
+// This is the sharpest case for the guard in AGENTS.md: every enumerator of
+// `data/` swallows ENOENT as "this channel has no videos", so a sync of a
+// channel on an unmounted platter reads the whole back catalogue as
+// undownloaded — and hands the download lane an instruction to re-fetch it all
+// onto the volume that was too full to hold it in the first place.
+//
+// `inspectChannelMedia` is the one module that can tell "nothing downloaded"
+// from "drive not mounted". A skip is not a failure: the bulk readout reports
+// both numbers and names the channel with the inspector's own sentence.
+test("Sync all skips a channel whose media drive is not mounted", async ({
+ page,
+}, testInfo) => {
+ test.setTimeout(90_000);
+ await resetData("one-youtube-channel-with-data");
+ await writeSettings({ minFreeDiskGB: 0 });
+ // A relocation whose drive went away: the link and the config agree with each
+ // other and with nothing on disk. Deliberately NOT created — an unmounted
+ // mountpoint whose parent is missing too is the honest version.
+ const target = join(testInfo.outputPath("never-mounted"), SLUG, "data");
+ await rm(dataDir(), { recursive: true, force: true });
+ await symlink(target, dataDir());
+ await writeChannelConfig(SLUG, { dataDir: target });
+
+ await page.goto("/channels");
+ await page.getByRole("button", { name: "sync every channel" }).click();
+ const result = page.getByLabel("sync all result");
+ await expect(result).toContainText(/Queued 0 . skipped 1/, {
+ timeout: 15_000,
+ });
+ // THE REASON NAMES THE DRIVE, which is the only thing the operator can act
+ // on — "skipped 1" alone would read as a bug in the sweep.
+ await expect(result).toHaveAttribute(
+ "title",
+ new RegExp(`${SLUG}: media unreachable:.*drive not mounted`),
+ );
+});