commit f0e532a02f5e303e574390b28c04a758f804fe64
parent 125ae785d7667c6146ee4c6d03ef30f93280f0ce
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 25 Sep 2026 17:00:34 -0400
common: the yt-dlp clip command shows with transcriptDownloads off
The operator (2026-09-25): "Oh no yt-dlp stays, I was only talking about the
zip downloads." The yt-dlp button copies a `yt-dlp --download-sections` line to
the clipboard and serves no file, so it is not a download: TranscriptModal now
renders it (both states) whatever `transcriptDownloads` says. The Download menu
and Copy MD stay behind the switch. Accessible names unchanged.
The siteSchema doc (SITE.md regenerated), the PlayerFeatures and homepage.ts
comments and the editor's site/hub form labels stop claiming the switch hides
the command; the site and hub transcript-downloads specs assert it present in
both states.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
9 files changed, 61 insertions(+), 47 deletions(-)
diff --git a/SITE.md b/SITE.md
@@ -188,7 +188,7 @@ Default: `true`
## `transcriptDownloads`
-Whether a visitor gets the per-video export controls in the transcript modal: the Download menu (txt / srt / json), Copy MD and Copy download command. Opt-OUT: absent/true = on, only an explicit `false` hides them. The machine contract (`/corpus.json`, manifests, shards, `llms.txt`) is served either way. The editor always shows them.
+Whether a visitor gets the per-video export controls in the transcript modal: the Download menu (txt / srt / json) and Copy MD. Opt-OUT: absent/true = on, only an explicit `false` hides them. The yt-dlp clip command is not a download (it copies a line, serves no file) and shows either way. The machine contract (`/corpus.json`, manifests, shards, `llms.txt`) is served either way. The editor always shows them.
Default: `true`
diff --git a/common/components/PlayerProvider.tsx b/common/components/PlayerProvider.tsx
@@ -113,8 +113,9 @@ type ClipState = {
// them from its site.json (`transcriptDownloads`); the editor mounts nothing
// and every host that passes nothing keeps everything on.
export type PlayerFeatures = {
- // The three per-video export controls: Copy download command, the Download
- // menu (txt / srt / json) and Copy MD.
+ // The two per-video export controls: the Download menu (txt / srt / json)
+ // and Copy MD. The yt-dlp clip command is not one of them — it copies a
+ // line, serves no file, and shows either way.
transcriptDownloads: boolean;
};
diff --git a/common/components/TranscriptModal.tsx b/common/components/TranscriptModal.tsx
@@ -74,8 +74,10 @@ export default function TranscriptModal() {
seekTo,
features,
} = usePlayer();
- // Off on a site whose site.json sets `transcriptDownloads: false`: the three
- // per-video export controls are not rendered at all (Share stays).
+ // Off on a site whose site.json sets `transcriptDownloads: false`: the two
+ // per-video export controls (the Download menu and Copy MD) are not rendered
+ // at all. The yt-dlp command stays: it copies a line to the clipboard and
+ // serves no file, so it is not a download. Share stays too.
const showDownloads = features.transcriptDownloads;
const { currentTime } = usePlayerTime();
const scrollRef = useRef<HTMLDivElement | null>(null);
@@ -404,28 +406,26 @@ export default function TranscriptModal() {
clipEnd={clipEnd}
onClear={clearClip}
/>
- {showDownloads && (
- <ControlButton
- title={
- canDownload
- ? copied
- ? "Copied!"
- : "Copy yt-dlp download command for this clip"
- : "Mark both clip start and end first"
- }
- onClick={onCopy}
- icon={
- copied ? (
- <CheckIcon className={iconSize} />
- ) : (
- <TerminalIcon className={iconSize} />
- )
- }
- label="yt-dlp"
- char={copied ? "\u2713" : "\u2193"}
- disabled={!canDownload}
- />
- )}
+ <ControlButton
+ title={
+ canDownload
+ ? copied
+ ? "Copied!"
+ : "Copy yt-dlp download command for this clip"
+ : "Mark both clip start and end first"
+ }
+ onClick={onCopy}
+ icon={
+ copied ? (
+ <CheckIcon className={iconSize} />
+ ) : (
+ <TerminalIcon className={iconSize} />
+ )
+ }
+ label="yt-dlp"
+ char={copied ? "\u2713" : "\u2193"}
+ disabled={!canDownload}
+ />
<Divider />
diff --git a/common/lib/homepage.ts b/common/lib/homepage.ts
@@ -33,8 +33,8 @@ export type HomepageConfig = {
// Cloudflare Pages project the hub deploys to (e.g. "archilyzer-hub"; never
// "archilyzer", the homepage's — deployHub refuses it).
cloudflareProject?: string;
- // The transcript modal's per-video export controls (Download menu, Copy MD,
- // Copy download command) on the hub's Browse and Ask pages — the same switch
+ // The transcript modal's per-video export controls (Download menu, Copy MD;
+ // the yt-dlp clip command shows either way) on the hub's Browse and Ask pages — the same switch
// as site.json `transcriptDownloads`. Opt-OUT: absent = on; only an explicit
// false is read, and only false is written.
transcriptDownloads?: boolean;
diff --git a/common/lib/siteSchema.ts b/common/lib/siteSchema.ts
@@ -120,7 +120,7 @@ export const SITE_FIELD_DOCS: FieldDocs<Site> = {
duplicates:
"Whether this site publishes the Duplicates page (and its header link). Opt-OUT: absent/true = on, only an explicit `false` hides it. Even when on, the page auto-hides when the site has no in-scope duplicate clusters.",
transcriptDownloads:
- "Whether a visitor gets the per-video export controls in the transcript modal: the Download menu (txt / srt / json), Copy MD and Copy download command. Opt-OUT: absent/true = on, only an explicit `false` hides them. The machine contract (`/corpus.json`, manifests, shards, `llms.txt`) is served either way. The editor always shows them.",
+ "Whether a visitor gets the per-video export controls in the transcript modal: the Download menu (txt / srt / json) and Copy MD. Opt-OUT: absent/true = on, only an explicit `false` hides them. The yt-dlp clip command is not a download (it copies a line, serves no file) and shows either way. The machine contract (`/corpus.json`, manifests, shards, `llms.txt`) is served either way. The editor always shows them.",
archiveMaxBytes:
"Per-site served-file size cap in bytes: any archive larger is dropped from what is served and flagged in the manifest, so a capped host (Cloudflare Pages: 25 MB) will not reject the deploy. 0 = no cap. Absent = the global default. Negative or non-numeric values are dropped.",
hubUrl:
diff --git a/editor/app/sites/components/HomepageConfigForm.tsx b/editor/app/sites/components/HomepageConfigForm.tsx
@@ -69,13 +69,12 @@ export function HomepageConfigForm({ config }: { config: HomepageConfig }) {
defaultChecked={config.transcriptDownloads !== false}
className="accent-brand"
/>
- Per-video transcript downloads (Download menu, Copy Markdown, Copy download
- command)
+ Per-video transcript downloads (Download menu, Copy Markdown)
</label>
<p className="-mt-2 text-xs text-muted-foreground">
On by default: the hub's transcript viewer offers the transcript as a
- file, as Markdown, and a yt-dlp command for a marked clip, for every
- archive it federates. Turn off to hide all three on the hub.
+ file and as Markdown, for every archive it federates. Turn off to hide
+ both on the hub; the yt-dlp command for a marked clip stays.
</p>
<div className="flex items-center gap-3">
<button
diff --git a/editor/app/sites/components/SiteForm.tsx b/editor/app/sites/components/SiteForm.tsx
@@ -297,13 +297,13 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) {
defaultChecked={initial.transcriptDownloads !== false}
className="accent-brand"
/>
- Per-video transcript downloads (Download menu, Copy Markdown, Copy download
- command)
+ Per-video transcript downloads (Download menu, Copy Markdown)
</label>
<p className="-mt-2 text-xs text-muted-foreground">
On by default: a visitor's transcript viewer offers the transcript as a
- txt/srt/json file, as Markdown, and a yt-dlp command for a marked clip.
- Turn off to hide all three on this site. The machine contract
+ txt/srt/json file and as Markdown. Turn off to hide both on this site;
+ the yt-dlp command for a marked clip is not a download and stays. The
+ machine contract
(corpus.json, manifests, shards) is published either way.
</p>
<label className="flex items-center gap-2 text-sm">
diff --git a/export/e2e-hub/transcript-downloads.spec.ts b/export/e2e-hub/transcript-downloads.spec.ts
@@ -26,7 +26,6 @@ test.afterEach(() => {
function exportControls(page: Page) {
return [
- page.getByRole("button", { name: "Mark both clip start and end first" }),
page.getByRole("button", { name: "Download this transcript as a file" }),
page.getByRole("button", {
name: "Copy this transcript as Markdown (for AI)",
@@ -34,6 +33,12 @@ function exportControls(page: Page) {
];
}
+// The yt-dlp clip command copies a line and serves no file, so it is not a
+// download: it shows whatever the switch says (unmarked state).
+function ytDlpButton(page: Page) {
+ return page.getByRole("button", { name: "Mark both clip start and end first" });
+}
+
// A bare channel/video id is same-origin content, which the hub reads from its
// own origin — installRoutes serves it.
async function openModal(page: Page) {
@@ -46,16 +51,17 @@ async function openModal(page: Page) {
await expect(page.locator("li", { hasText: "beta line" })).toBeVisible();
}
-test("hub, no homepage.json key: the three export controls are present", async ({
+test("hub, no homepage.json key: the export controls and yt-dlp are present", async ({
page,
}) => {
await openModal(page);
for (const control of exportControls(page)) {
await expect(control).toBeVisible();
}
+ await expect(ytDlpButton(page)).toBeVisible();
});
-test("hub, homepage.json transcriptDownloads: false hides all three; Share stays", async ({
+test("hub, homepage.json transcriptDownloads: false hides both; yt-dlp and Share stay", async ({
page,
}) => {
fs.mkdirSync(path.dirname(HOMEPAGE_FILE), { recursive: true });
@@ -64,6 +70,7 @@ test("hub, homepage.json transcriptDownloads: false hides all three; Share stays
for (const control of exportControls(page)) {
await expect(control).toHaveCount(0);
}
+ await expect(ytDlpButton(page)).toBeVisible();
await expect(
page.getByRole("button", { name: "Copy share link at current time" }),
).toBeVisible();
diff --git a/export/e2e/transcript-downloads.spec.ts b/export/e2e/transcript-downloads.spec.ts
@@ -4,8 +4,8 @@ import { expect, test, type Page } from "@playwright/test";
import { CHANNEL_SLUG, VIDEO_TRANSCRIPT_ONLY } from "./fixtures/data";
import { expectModalOpen, installRoutes } from "./helpers";
-// site.json `transcriptDownloads` gates the transcript modal's three per-video
-// export controls. The suite serves ONE site (SITE_ID=testsite, SITES_DIR =
+// site.json `transcriptDownloads` gates the transcript modal's two per-video
+// export controls (Download menu, Copy MD); the yt-dlp clip command is not one. The suite serves ONE site (SITE_ID=testsite, SITES_DIR =
// e2e/fixtures/sites) and currentSite() re-reads site.json on every dev render,
// so the off case flips the key in the fixture for the duration of one test and
// puts the file back. A run killed mid-test leaves the key behind;
@@ -35,11 +35,10 @@ test.afterEach(() => {
fs.writeFileSync(SITE_FILE, pristine);
});
-// The three controls, by the accessible names they carry once the transcript
+// The two controls, by the accessible names they carry once the transcript
// has loaded and no clip is marked.
function exportControls(page: Page) {
return [
- page.getByRole("button", { name: "Mark both clip start and end first" }),
page.getByRole("button", { name: "Download this transcript as a file" }),
page.getByRole("button", {
name: "Copy this transcript as Markdown (for AI)",
@@ -47,6 +46,12 @@ function exportControls(page: Page) {
];
}
+// The yt-dlp clip command copies a line and serves no file, so it is not a
+// download: it shows whatever the switch says (unmarked state).
+function ytDlpButton(page: Page) {
+ return page.getByRole("button", { name: "Mark both clip start and end first" });
+}
+
async function openModal(page: Page) {
await installRoutes(page);
await page.goto(`/?v=${CHANNEL_SLUG}/${VIDEO_TRANSCRIPT_ONLY}`);
@@ -55,19 +60,20 @@ async function openModal(page: Page) {
await expect(page.locator("li", { hasText: "beta line" })).toBeVisible();
}
-test("default (key absent): the three export controls are present", async ({
+test("default (key absent): the export controls and yt-dlp are present", async ({
page,
}) => {
await openModal(page);
for (const control of exportControls(page)) {
await expect(control).toBeVisible();
}
+ await expect(ytDlpButton(page)).toBeVisible();
await expect(
page.getByRole("button", { name: "Copy share link at current time" }),
).toBeVisible();
});
-test("transcriptDownloads: false hides all three; Share stays", async ({
+test("transcriptDownloads: false hides both; yt-dlp and Share stay", async ({
page,
}) => {
writeSiteFile({ ...JSON.parse(pristine), transcriptDownloads: false });
@@ -75,6 +81,7 @@ test("transcriptDownloads: false hides all three; Share stays", async ({
for (const control of exportControls(page)) {
await expect(control).toHaveCount(0);
}
+ await expect(ytDlpButton(page)).toBeVisible();
// Nothing else in the bar went with them.
await expect(
page.getByRole("button", { name: "Copy share link at current time" }),