Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 7ce693ef86310088415fa61fa19288316c81fae6
parent c5cbd692de8e8ef1822098fe30e6f686fbe722cb
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 15 May 2026 19:31:35 -0400

toggle channels in editor

Diffstat:
M.gitignore | 1+
Mcommon/controller/buildIndex.ts | 4++++
Mcommon/lib/channelConfig.ts | 4++++
Mcommon/ytdlp/runYtdlp.ts | 3+++
Meditor/app/channels/actions.ts | 17+++++++++++++++++
Aeditor/app/channels/components/ChannelBuildToggle.tsx | 43+++++++++++++++++++++++++++++++++++++++++++
Meditor/app/channels/page.tsx | 13++++++++++++-
Aeditor/e2e/channel-build-toggle.spec.ts | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Meditor/e2e/helpers.ts | 2+-
Meditor/package.json | 4++--
Meditor/playwright.config.ts | 4++--
Dexport/public/subs/manifest.json | 2--
12 files changed, 190 insertions(+), 8 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -50,6 +50,7 @@ yarn-error.log* /export/public/transcripts.tar.xz # preprocessed transcript data (regenerate with `pnpm build:index`) +/export/public/subs/ /export/public/summaries/ /export/public/transcripts/ /transcripts/index.mdb/ diff --git a/common/controller/buildIndex.ts b/common/controller/buildIndex.ts @@ -156,6 +156,10 @@ async function scanSource( log(`Skipping channel ${ch.name}: missing or invalid config.json`); continue; } + if (cfg.excludeFromBuild) { + log(`Skipping channel ${ch.name}: excluded from build`); + continue; + } channels.set(ch.name, cfg); const dataDir = path.join(channelDir, "data"); let videoEntries: Dirent[]; diff --git a/common/lib/channelConfig.ts b/common/lib/channelConfig.ts @@ -16,6 +16,7 @@ export type ChannelConfig = { subLangs?: string; lastSyncedAt?: string; lastFullDownloadAt?: string; + excludeFromBuild?: boolean; }; export const HANDLING_VALUES: ReadonlyArray<ChannelHandling> = [ @@ -66,5 +67,8 @@ export function parseChannelConfig(raw: unknown): ChannelConfig | null { if (typeof r.lastFullDownloadAt === "string") { config.lastFullDownloadAt = r.lastFullDownloadAt; } + if (typeof r.excludeFromBuild === "boolean") { + config.excludeFromBuild = r.excludeFromBuild; + } return config; } diff --git a/common/ytdlp/runYtdlp.ts b/common/ytdlp/runYtdlp.ts @@ -126,6 +126,9 @@ const OUTPUT_ARGS: string[] = [ "subtitle:data/%(id)s/transcript", "-o", "infojson:data/%(id)s/metadata", + // Suppress per-playlist info.json/description files that otherwise land at + // the channel root and trip up the index build with malformed entries. + "--no-write-playlist-metafiles", ]; async function storePlaylist(opts: RunYtdlpOpts): Promise<void> { diff --git a/editor/app/channels/actions.ts b/editor/app/channels/actions.ts @@ -95,6 +95,23 @@ export async function refreshChannelSnapshotAction( return undefined; } +export async function toggleChannelBuildInclusionAction( + slug: string, +): Promise<ActionResult> { + const paths = getPaths(); + const existing = await readChannelConfig(paths, slug); + if (!existing) return { error: `Channel "${slug}" not found` }; + const next = { ...existing }; + if (existing.excludeFromBuild) { + delete next.excludeFromBuild; + } else { + next.excludeFromBuild = true; + } + await writeChannelConfig(paths, slug, next); + revalidatePath("/channels"); + return undefined; +} + export async function deleteChannelAction( slug: string, _prev: ActionResult, diff --git a/editor/app/channels/components/ChannelBuildToggle.tsx b/editor/app/channels/components/ChannelBuildToggle.tsx @@ -0,0 +1,43 @@ +import { toggleChannelBuildInclusionAction } from "../actions"; + +export function ChannelBuildToggle({ + slug, + excluded, +}: { + slug: string; + excluded: boolean; +}) { + async function submit() { + "use server"; + await toggleChannelBuildInclusionAction(slug); + } + return ( + <form action={submit}> + <button + type="submit" + aria-label={`toggle build inclusion for ${slug}`} + aria-pressed={!excluded} + title={ + excluded + ? "Excluded from the next export build. Click to include." + : "Included in the next export build. Click to exclude." + } + className={ + "inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium border transition-colors " + + (excluded + ? "border-zinc-300 dark:border-zinc-700 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800" + : "border-emerald-300 dark:border-emerald-800 bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300 hover:bg-emerald-100 dark:hover:bg-emerald-900/40") + } + > + <span + aria-hidden="true" + className={ + "inline-block h-1.5 w-1.5 rounded-full " + + (excluded ? "bg-zinc-400" : "bg-emerald-500") + } + /> + {excluded ? "Skipped" : "Included"} + </button> + </form> + ); +} diff --git a/editor/app/channels/page.tsx b/editor/app/channels/page.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { listChannels } from "yt-dlp-transcript-common/controller/channels"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { ChannelAvailabilityButton } from "./components/ChannelAvailabilityButton"; +import { ChannelBuildToggle } from "./components/ChannelBuildToggle"; import { ChannelSyncButton } from "./components/ChannelSyncButton"; export const dynamic = "force-dynamic"; @@ -34,6 +35,7 @@ export default async function ChannelsPage() { <Th>Slug</Th> <Th>Name</Th> <Th>Handling</Th> + <Th>Build</Th> <Th className="text-right">Playlist</Th> <Th className="whitespace-nowrap">Last sync</Th> <Th className="text-right">Downloads</Th> @@ -45,7 +47,10 @@ export default async function ChannelsPage() { {channels.map((c) => ( <tr key={c.slug} - className="border-t border-zinc-200 dark:border-zinc-800" + className={ + "border-t border-zinc-200 dark:border-zinc-800 " + + (c.config.excludeFromBuild ? "opacity-60" : "") + } > <Td className="font-mono"> <Link @@ -57,6 +62,12 @@ export default async function ChannelsPage() { </Td> <Td>{c.config.name ?? ""}</Td> <Td>{c.config.handling}</Td> + <Td> + <ChannelBuildToggle + slug={c.slug} + excluded={c.config.excludeFromBuild === true} + /> + </Td> <Td className="text-right" ariaLabel={`playlist count for ${c.slug}`} diff --git a/editor/e2e/channel-build-toggle.spec.ts b/editor/e2e/channel-build-toggle.spec.ts @@ -0,0 +1,101 @@ +import { test, expect } from "@playwright/test"; +import { pathExists, readJson, resetData } from "./helpers"; +import type { ChannelConfig } from "yt-dlp-transcript-common/lib/channelConfig"; + +// The build-inclusion toggle on /channels flips `excludeFromBuild` in the +// channel's config.json. The next build then skips the channel entirely and +// the existing top-level cleanup loops in buildIndex remove its per-channel +// output dirs. Toggling back re-includes it on the next build. + +const CONFIG_PATH = "test-transcripts/channels/test-youtube/config.json"; +const MANIFEST_PATH = "test-transcripts/.export-public/summaries/manifest.json"; +const CHANNEL_OUT_DIR = + "test-transcripts/.export-public/transcripts/test-youtube"; + +async function runBuild(page: import("@playwright/test").Page) { + await page.goto("/build"); + await page.getByRole("button", { name: "Build index" }).click(); + await expect(page.getByLabel("Build index output")).toContainText("Done", { + timeout: 20_000, + }); +} + +test("toggling a channel off skips it from the build; toggling back restores it", async ({ + page, +}) => { + await resetData("one-youtube-channel-with-data"); + + await runBuild(page); + { + const manifest = await readJson<{ + totalCount: number; + channels: { name: string }[]; + }>(MANIFEST_PATH); + expect(manifest.totalCount).toBe(1); + expect(manifest.channels.map((c) => c.name)).toContain( + "Test YouTube Channel", + ); + expect(await pathExists(`${CHANNEL_OUT_DIR}/manifest.json`)).toBe(true); + } + + await page.goto("/channels"); + const toggle = page.getByRole("button", { + name: "toggle build inclusion for test-youtube", + }); + await expect(toggle).toHaveText(/Included/); + await expect(toggle).toHaveAttribute("aria-pressed", "true"); + await toggle.click(); + + await expect( + page.getByRole("button", { name: "toggle build inclusion for test-youtube" }), + ).toHaveText(/Skipped/); + await expect( + page.getByRole("button", { name: "toggle build inclusion for test-youtube" }), + ).toHaveAttribute("aria-pressed", "false"); + + { + const cfg = await readJson<ChannelConfig>(CONFIG_PATH); + expect(cfg.excludeFromBuild).toBe(true); + } + + await runBuild(page); + { + const manifest = await readJson<{ + totalCount: number; + channels: { name: string }[]; + }>(MANIFEST_PATH); + expect(manifest.totalCount).toBe(0); + expect(manifest.channels.map((c) => c.name)).not.toContain( + "Test YouTube Channel", + ); + expect(await pathExists(CHANNEL_OUT_DIR)).toBe(false); + } + + await page.goto("/channels"); + const reToggle = page.getByRole("button", { + name: "toggle build inclusion for test-youtube", + }); + await expect(reToggle).toHaveText(/Skipped/); + await reToggle.click(); + await expect( + page.getByRole("button", { name: "toggle build inclusion for test-youtube" }), + ).toHaveText(/Included/); + + { + const cfg = await readJson<ChannelConfig>(CONFIG_PATH); + expect(cfg.excludeFromBuild).toBeUndefined(); + } + + await runBuild(page); + { + const manifest = await readJson<{ + totalCount: number; + channels: { name: string }[]; + }>(MANIFEST_PATH); + expect(manifest.totalCount).toBe(1); + expect(manifest.channels.map((c) => c.name)).toContain( + "Test YouTube Channel", + ); + expect(await pathExists(`${CHANNEL_OUT_DIR}/manifest.json`)).toBe(true); + } +}); diff --git a/editor/e2e/helpers.ts b/editor/e2e/helpers.ts @@ -19,7 +19,7 @@ const defaultTestSettingsFile = resolve( "test-settings.default.json", ); -const baseUrl = process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:3001"; +const baseUrl = process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:3011"; async function fileExists(p: string): Promise<boolean> { try { diff --git a/editor/package.json b/editor/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "dev": "next dev --port 3001", - "dev:test": "TRANSCRIPTS_DIR=$(pwd)/test-transcripts EXPORT_PUBLIC_DIR=$(pwd)/test-transcripts/.export-public SETTINGS_FILE=$(pwd)/test-settings.json YTDLP_BIN=$(pwd)/e2e/fixtures/bin/fake-ytdlp.mjs WHISPER_BIN=$(pwd)/e2e/fixtures/bin/fake-whisper.mjs WHISPER_MODEL=/dev/null FFMPEG_BIN=$(pwd)/e2e/fixtures/bin/fake-ffmpeg.mjs next dev --port 3001", - "start:test": "TRANSCRIPTS_DIR=$(pwd)/test-transcripts EXPORT_PUBLIC_DIR=$(pwd)/test-transcripts/.export-public SETTINGS_FILE=$(pwd)/test-settings.json YTDLP_BIN=$(pwd)/e2e/fixtures/bin/fake-ytdlp.mjs WHISPER_BIN=$(pwd)/e2e/fixtures/bin/fake-whisper.mjs WHISPER_MODEL=/dev/null FFMPEG_BIN=$(pwd)/e2e/fixtures/bin/fake-ffmpeg.mjs next start --port 3001", + "dev:test": "TRANSCRIPTS_DIR=$(pwd)/test-transcripts EXPORT_PUBLIC_DIR=$(pwd)/test-transcripts/.export-public SETTINGS_FILE=$(pwd)/test-settings.json YTDLP_BIN=$(pwd)/e2e/fixtures/bin/fake-ytdlp.mjs WHISPER_BIN=$(pwd)/e2e/fixtures/bin/fake-whisper.mjs WHISPER_MODEL=/dev/null FFMPEG_BIN=$(pwd)/e2e/fixtures/bin/fake-ffmpeg.mjs next dev --port 3011", + "start:test": "TRANSCRIPTS_DIR=$(pwd)/test-transcripts EXPORT_PUBLIC_DIR=$(pwd)/test-transcripts/.export-public SETTINGS_FILE=$(pwd)/test-settings.json YTDLP_BIN=$(pwd)/e2e/fixtures/bin/fake-ytdlp.mjs WHISPER_BIN=$(pwd)/e2e/fixtures/bin/fake-whisper.mjs WHISPER_MODEL=/dev/null FFMPEG_BIN=$(pwd)/e2e/fixtures/bin/fake-ffmpeg.mjs next start --port 3011", "build": "next build", "start": "next start --port 3001", "lint": "eslint", diff --git a/editor/playwright.config.ts b/editor/playwright.config.ts @@ -1,11 +1,11 @@ import { defineConfig, devices } from "@playwright/test"; -const PORT = Number(process.env.PORT ?? 3001); +const PORT = Number(process.env.PORT ?? 3011); const baseURL = `http://localhost:${PORT}`; const webServerCommand = process.env.E2E_MODE === "start" ? "pnpm start:test" : "pnpm dev:test"; -const EXPORT_PORT = Number(process.env.EXPORT_PORT ?? 3000); +const EXPORT_PORT = Number(process.env.EXPORT_PORT ?? 3010); const exportBaseURL = `http://localhost:${EXPORT_PORT}`; export default defineConfig({ diff --git a/export/public/subs/manifest.json b/export/public/subs/manifest.json @@ -1 +0,0 @@ -{"version":1,"channels":[],"totalCount":0,"generatedAt":"2026-05-15T00:29:24.351Z"} -\ No newline at end of file