commit 3c6332662dd4ae49d5af7fa2301b9afbd499fccf parent 14b4b3620a4d6fe2b1aa81f3389dcba3c127a21a Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st> Date: Tue, 9 Jun 2026 10:46:49 -0400 global parallel default and split metadata+filter pipeline Diffstat:
25 files changed, 676 insertions(+), 51 deletions(-)
diff --git a/common/controller/channelSnapshot.ts b/common/controller/channelSnapshot.ts @@ -20,6 +20,7 @@ import { resolveEffectiveAvailability, } from "../lib/availability-server"; import { isDoNotClean } from "../lib/doNotClean-server"; +import { loadDownloadOutcome } from "../lib/downloadOutcome-server"; import type { Paths } from "../lib/paths"; import { extractVideoId } from "../ytdlp/runYtdlp"; import { reconcileVideoDirs } from "./reconcileVideoDirs"; @@ -67,6 +68,11 @@ export type ChannelSnapshot = { // transcript or the canonical transcript.en.vtt. Optional: older snapshots // lack it; readers must default to []. nonStandardVtt: string[]; + // Videos whose latest download-outcome.json is "skipped-filtered" (e.g. + // skip-live) and that still have no downloaded artifact — i.e. they were + // declined as currently-live/upcoming and will be retried on a later sync. + // Optional: older snapshots lack it; readers must default to []. + skippedByFilter: string[]; }; undownloadedIds: string[]; excludedFromDownload?: ExcludedFromDownload; @@ -229,6 +235,7 @@ export async function generateChannelSnapshot( const availability = await loadAvailability(dir); const effectiveAvailability = await resolveEffectiveAvailability(dir); const doNotClean = await isDoNotClean(dir); + const outcome = await loadDownloadOutcome(dir); return { id, files, @@ -237,6 +244,7 @@ export async function generateChannelSnapshot( availability, effectiveAvailability, doNotClean, + outcome, }; }), ), @@ -284,14 +292,23 @@ export async function generateChannelSnapshot( const noMetadata: string[] = []; const partialDownloads: string[] = []; const nonStandardVtt: string[] = []; + const skippedByFilter: string[] = []; let transcribedWithAudioBytes = 0; let multipleAudioFormatsBytes = 0; let transcribed = 0; let downloaded = 0; - for (const { id, files, audioSizes } of perVideo) { + for (const { id, files, audioSizes, outcome } of perVideo) { if (isVideoTranscribed(files)) transcribed++; if (isVideoDownloaded(files)) downloaded++; if (!files.hasMeta && !excludedById.has(id)) noMetadata.push(id); + // A video the filter declined (e.g. live/upcoming) that hasn't since been + // downloaded. Once it lands an artifact it drops out of this bucket. + if ( + outcome?.status === "skipped-filtered" && + !videoHasAnyArtifact(files) + ) { + skippedByFilter.push(id); + } if ( targetAudioFile && files.audioFiles.length > 0 && @@ -409,6 +426,7 @@ export async function generateChannelSnapshot( duplicateDirs: [], partialDownloads: partialDownloads.sort(), nonStandardVtt: nonStandardVtt.sort(), + skippedByFilter: skippedByFilter.sort(), }, undownloadedIds, excludedFromDownload, diff --git a/common/controller/whisperBatch.ts b/common/controller/whisperBatch.ts @@ -7,6 +7,7 @@ import { isVideoTranscribed, readVideoFiles } from "../lib/videoStatus"; import { transcribeOneVideo } from "./transcribeOne"; import { resolveShardItems } from "./shard"; import { countNotYetTranscribed } from "./channels"; +import { getSettings } from "../lib/settings"; import type { TaskTracker } from "../jobs/taskHooks"; import type { JobProgress } from "../jobs/registry"; @@ -75,7 +76,7 @@ export async function runWhisperBatch({ const channelDir = path.join(paths.channelsDir, channelSlug); const dataDir = path.join(channelDir, "data"); const failureListFile = path.join(channelDir, "failed-transcriptions"); - const limit = pLimit(concurrency ?? paths.parallelTranscribeLimit); + const limit = pLimit(concurrency ?? getSettings().parallelTranscriptions); const resolvedAudioFilename = audioFilename ?? (audioFormat ? `audio.${audioFormat}` : "audio.mp3"); const strict = strictAudioFormat && audioFormat !== undefined; diff --git a/common/lib/channelConfig.ts b/common/lib/channelConfig.ts @@ -28,6 +28,10 @@ export type ChannelConfig = { // omitted, the global SiteSettings value is used. 0 disables the sleep // for this channel. sleepBetweenDownloadsSeconds?: number; + // Per-channel override for the global skipLiveDownloads setting. When + // omitted, the global SiteSettings value is used. Set false to allow this + // channel to download currently-live/upcoming videos. + skipLiveDownloads?: boolean; // Opt-in audio-integrity checking for sources that intermittently serve // corrupt audio mid-download (e.g. Odysee "original" format). When // enabled, the managed downloader periodically validates the in-progress @@ -109,6 +113,9 @@ export function parseChannelConfig(raw: unknown): ChannelConfig | null { if (typeof r.excludeFromSync === "boolean") { config.excludeFromSync = r.excludeFromSync; } + if (typeof r.skipLiveDownloads === "boolean") { + config.skipLiveDownloads = r.skipLiveDownloads; + } if ( typeof r.sleepBetweenDownloadsSeconds === "number" && Number.isFinite(r.sleepBetweenDownloadsSeconds) && diff --git a/common/lib/downloadFilters.ts b/common/lib/downloadFilters.ts @@ -0,0 +1,83 @@ +// App-level download filters: pure decisions made from a video's prefetched +// metadata, evaluated by the managed downloader BEFORE it commits to a full +// download. The metadata/download split (see prefetchMetadata in +// downloadOneManaged.ts) exists so these filters have something to look at. +// +// A filter that returns `skip: true` causes the managed downloader to record a +// "skipped-filtered" outcome and move on WITHOUT archiving the video, so the +// next sync/download-missing retries it once the filter no longer matches. +// +// Adding a filter = append one entry to FILTERS. Each filter sees the same +// context (metadata + channel config + resolved settings). + +import type { ChannelConfig } from "./channelConfig"; +import type { RawMetadata } from "./transcripts-server"; + +export type DownloadFilterSettings = { + // Global default-on toggle; per-channel config can override it. + skipLiveDownloads: boolean; +}; + +export type DownloadFilterContext = { + metadata: RawMetadata | null; + channelConfig: ChannelConfig; + settings: DownloadFilterSettings; +}; + +export type DownloadFilterDecision = { + skip: boolean; + filter: string; + reason: string; +}; + +type DownloadFilter = { + name: string; + // Returns a skip decision, or null when the filter doesn't apply / passes. + evaluate(ctx: DownloadFilterContext): DownloadFilterDecision | null; +}; + +// Resolve the skip-live toggle: per-channel override wins over the global +// default. +function skipLiveEnabled(ctx: DownloadFilterContext): boolean { + return ctx.channelConfig.skipLiveDownloads ?? ctx.settings.skipLiveDownloads; +} + +// Skip videos that are currently live or scheduled/upcoming — they can't be +// downloaded as a finished video yet. IMPORTANT: a finished livestream VOD +// (was_live === true / live_status === "was_live") is a normal downloadable +// video and must NOT be skipped. This deliberately diverges from the +// `isLivestream` flag in transcripts-server.ts:summarize(), which folds +// was_live in for *display* purposes — do not unify the two. +const skipLive: DownloadFilter = { + name: "skipLive", + evaluate(ctx) { + if (!skipLiveEnabled(ctx)) return null; + const m = ctx.metadata; + if (!m) return null; // fail-open: no metadata → don't skip + const status = m.live_status ?? ""; + const isLive = m.is_live === true || status === "is_live"; + const isUpcoming = status === "is_upcoming"; + if (!isLive && !isUpcoming) return null; + return { + skip: true, + filter: "skipLive", + reason: isUpcoming + ? "video is an upcoming/scheduled livestream" + : "video is currently live", + }; + }, +}; + +const FILTERS: ReadonlyArray<DownloadFilter> = [skipLive]; + +// Evaluate every filter; the first one that says "skip" wins. Returns null when +// the video passes all filters. +export function evaluateDownloadFilters( + ctx: DownloadFilterContext, +): DownloadFilterDecision | null { + for (const f of FILTERS) { + const decision = f.evaluate(ctx); + if (decision?.skip) return decision; + } + return null; +} diff --git a/common/lib/downloadOutcome.ts b/common/lib/downloadOutcome.ts @@ -10,7 +10,11 @@ export type DownloadOutcomeStatus = | "ok-auto-transcribed" | "ok-audio-checked" | "failed" - | "failed-corrupt-source"; + | "failed-corrupt-source" + // The app-level filter pass (e.g. skip-live) declined to download this video. + // Not a failure and not archived — the next sync/download-missing retries it + // once the filter no longer matches (e.g. a live stream becomes a VOD). + | "skipped-filtered"; export const DOWNLOAD_OUTCOME_STATUS_VALUES: ReadonlyArray<DownloadOutcomeStatus> = [ "ok", @@ -19,13 +23,17 @@ export const DOWNLOAD_OUTCOME_STATUS_VALUES: ReadonlyArray<DownloadOutcomeStatus "ok-audio-checked", "failed", "failed-corrupt-source", + "skipped-filtered", ]; export type DownloadAttemptKind = | "primary" | "auth-retry" | "no-subs-fallback" - | "audio-checked-primary"; + | "audio-checked-primary" + // The metadata-only pass that runs before the real download so app-level + // filters can decide, and so the download can reuse it via --load-info-json. + | "metadata-prefetch"; export type AudioCheckProbeVerdict = "clean" | "partial" | "malformed"; @@ -37,7 +45,8 @@ export type AudioCheckAttemptStats = { }; export type DownloadAttempt = { - n: 1 | 2 | 3; + // 0 = the metadata-prefetch pass; 1..3 = the real download attempts. + n: number; kind: DownloadAttemptKind; handling: ChannelHandling; usedCookies: boolean; @@ -55,6 +64,9 @@ export type DownloadOutcomeRecord = { finishedAt: string; attempts: DownloadAttempt[]; fellBackToTranscribe?: boolean; + // Set when status is "skipped-filtered": which app-level filter declined the + // download and why. Recorded so the UI/log can explain the skip. + filter?: { name: string; reason: string }; }; export const DOWNLOAD_OUTCOME_FILENAME = "download-outcome.json"; diff --git a/common/lib/paths.ts b/common/lib/paths.ts @@ -37,7 +37,6 @@ export type Paths = { whisperBin: string; whisperModel: string; ffmpegBin: string; - parallelTranscribeLimit: number; }; let cached: Paths | null = null; @@ -92,9 +91,6 @@ export function getPaths(): Paths { "ggml-base.en.bin", ), ffmpegBin: process.env.FFMPEG_BIN ?? "ffmpeg", - parallelTranscribeLimit: process.env.PARALLEL_TRANSCRIBE_LIMIT - ? Number(process.env.PARALLEL_TRANSCRIBE_LIMIT) - : 4, }; return cached; } diff --git a/common/lib/settings.ts b/common/lib/settings.ts @@ -22,11 +22,22 @@ export type SiteSettings = { // within one invocation, so without this the managed loop hammers the // source IP back-to-back. 0 disables. Per-channel override available. sleepBetweenDownloadsSeconds: number; + // Default number of videos transcribed in parallel when a "Transcribe + // missing" / bucket run doesn't specify its own concurrency. The per-run + // Concurrency input in the channel UI overrides this for a single run. + parallelTranscriptions: number; // When true, the no-subs fallback in the managed downloader runs whisper // inline immediately after the audio download succeeds. When false // (default), audio is left for the next "Transcribe missing" pass so a // batch download finishes faster and whisper can parallelize. inlineTranscribeOnFallback: boolean; + // When true (default), managed downloads skip videos that are currently live + // or scheduled/upcoming, decided from a metadata-only prefetch pass. Finished + // livestream VODs (was_live) are NOT skipped and download normally. A skip is + // recorded but not archived, so the next sync/download-missing retries the + // video once the stream ends. Per-channel override available + // (ChannelConfig.skipLiveDownloads). + skipLiveDownloads: boolean; // Default social links applied to every site that doesn't define its own. // A site inherits these unless its site.json carries an explicit // `socialLinks` array — see Site.socialLinks / resolveSocialLinks in @@ -44,6 +55,9 @@ export type SocialLink = { export const SLEEP_BETWEEN_DOWNLOADS_MAX_SECONDS = 600; export const SLEEP_BETWEEN_DOWNLOADS_DEFAULT_SECONDS = 10; +export const PARALLEL_TRANSCRIPTIONS_MAX = 16; +export const PARALLEL_TRANSCRIPTIONS_DEFAULT = 2; + export const TRANSCRIPT_PAGE_HARD_CAP_BYTES = 20 * 1024 * 1024; export const TRANSCRIPT_PAGE_MIN_BYTES = 256 * 1024; export const TRANSCRIPT_PAGE_DEFAULT_BYTES = 8 * 1024 * 1024; @@ -80,7 +94,9 @@ function defaults(): SiteSettings { transcribeModel: paths.whisperModel, cookiesFromBrowser: "", sleepBetweenDownloadsSeconds: SLEEP_BETWEEN_DOWNLOADS_DEFAULT_SECONDS, + parallelTranscriptions: PARALLEL_TRANSCRIPTIONS_DEFAULT, inlineTranscribeOnFallback: false, + skipLiveDownloads: true, socialLinks: [], }; } @@ -149,6 +165,16 @@ export function clampSleepBetweenDownloadsSeconds(value: unknown): number { return n; } +export function clampParallelTranscriptions(value: unknown): number { + const n = + typeof value === "number" && Number.isFinite(value) + ? Math.floor(value) + : PARALLEL_TRANSCRIPTIONS_DEFAULT; + if (n < 1) return 1; + if (n > PARALLEL_TRANSCRIPTIONS_MAX) return PARALLEL_TRANSCRIPTIONS_MAX; + return n; +} + export function getSettings(): SiteSettings { const file = getPaths().settingsFile; let parsed: Partial<SiteSettings> = {}; @@ -185,9 +211,15 @@ export function getSettings(): SiteSettings { merged.sleepBetweenDownloadsSeconds = clampSleepBetweenDownloadsSeconds( merged.sleepBetweenDownloadsSeconds, ); + merged.parallelTranscriptions = clampParallelTranscriptions( + merged.parallelTranscriptions, + ); if (typeof merged.inlineTranscribeOnFallback !== "boolean") { merged.inlineTranscribeOnFallback = false; } + if (typeof merged.skipLiveDownloads !== "boolean") { + merged.skipLiveDownloads = true; + } merged.socialLinks = parseSocialLinks(merged.socialLinks); return merged; } @@ -259,7 +291,11 @@ export async function writeSettings(next: SiteSettings): Promise<void> { sleepBetweenDownloadsSeconds: clampSleepBetweenDownloadsSeconds( next.sleepBetweenDownloadsSeconds, ), + parallelTranscriptions: clampParallelTranscriptions( + next.parallelTranscriptions, + ), inlineTranscribeOnFallback: next.inlineTranscribeOnFallback === true, + skipLiveDownloads: next.skipLiveDownloads !== false, socialLinks, }; const tmp = `${file}.tmp-${process.pid}`; diff --git a/common/lib/transcripts-server.ts b/common/lib/transcripts-server.ts @@ -1,3 +1,5 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; import { formatDate, formatDuration } from "./format"; import type { DisplaySummary, Platform, TranscriptSummary } from "./transcripts"; import type { VideoStat } from "./stats"; @@ -29,6 +31,29 @@ export type RawMetadata = { language?: string; }; +// Read and parse a video's metadata.info.json into the typed RawMetadata +// shape. Returns null on a missing/unreadable/invalid file. The canonical +// loader for the four spots that previously parsed it inline. +export async function loadRawMetadata( + infoJsonPath: string, +): Promise<RawMetadata | null> { + try { + const raw = await readFile(infoJsonPath, "utf8"); + const parsed = JSON.parse(raw); + if (!parsed || typeof parsed !== "object") return null; + return parsed as RawMetadata; + } catch { + return null; + } +} + +// Convenience: load metadata.info.json from a video's data dir. +export function loadRawMetadataFromDir( + videoDir: string, +): Promise<RawMetadata | null> { + return loadRawMetadata(path.join(videoDir, "metadata.info.json")); +} + function detectPlatform(meta: RawMetadata): Platform { const key = meta.extractor_key ?? meta.extractor ?? ""; if (/^rumble/i.test(key)) return "rumble"; diff --git a/common/ytdlp/downloadOneManaged.ts b/common/ytdlp/downloadOneManaged.ts @@ -17,6 +17,8 @@ import { type DownloadOutcomeStatus, } from "../lib/downloadOutcome"; import { writeDownloadOutcome } from "../lib/downloadOutcome-server"; +import { loadRawMetadata } from "../lib/transcripts-server"; +import { evaluateDownloadFilters } from "../lib/downloadFilters"; import type { Paths } from "../lib/paths"; import { transcribeOneVideo } from "../controller/transcribeOne"; import { extractVideoId, outputArgsForUrl } from "./runYtdlp"; @@ -60,35 +62,57 @@ export type ManagedDownloadOpts = { // next "Transcribe missing" pass. Resolved from the site setting by the // playlist-level caller. inlineTranscribeOnFallback?: boolean; + // Resolved global SiteSettings.skipLiveDownloads (default true). Combined + // with the per-channel override (channelConfig.skipLiveDownloads) inside the + // download filters. Plumbed from the playlist-level caller alongside the + // other resolved settings. + globalSkipLiveDownloads?: boolean; }; -function youtubeHandlingArgs(config: ChannelConfig): string[] { - return [ +// When `reuseInfoJson` is true, the real download reuses the metadata the +// prefetch pass already wrote (fed in via sourceArgs as --load-info-json), so +// we drop --write-info-json (the file is already on disk). When false, the +// legacy single-call behavior: write the info json during this download. +function youtubeHandlingArgs( + config: ChannelConfig, + reuseInfoJson = false, +): string[] { + const args = [ "--write-auto-subs", "--write-subs", "--sub-langs", config.subLangs ?? "en.*,live_chat", - "--write-info-json", - "--skip-download", - "-t", - "sleep", ]; + if (!reuseInfoJson) args.push("--write-info-json"); + args.push("--skip-download", "-t", "sleep"); + return args; } -function transcribeHandlingArgs(config: ChannelConfig): string[] { +function transcribeHandlingArgs( + config: ChannelConfig, + reuseInfoJson = false, +): string[] { const fmt: AudioFormat = config.audioFormat ?? "mp3"; - const args = [ - "--write-info-json", - "-f", - "bestaudio/worst", - "-x", - "--audio-format", - fmt, - ]; + const args: string[] = []; + if (!reuseInfoJson) args.push("--write-info-json"); + args.push("-f", "bestaudio/worst", "-x", "--audio-format", fmt); if (config.keepSourceVideo) args.push("-k"); return args; } +function handlingArgs(config: ChannelConfig, reuseInfoJson: boolean): string[] { + return config.handling === "youtube" + ? youtubeHandlingArgs(config, reuseInfoJson) + : transcribeHandlingArgs(config, reuseInfoJson); +} + +// The source a download attempt reads from: either the prefetched info json +// (no positional URL needed) or the URL itself. Mirrors how the no-subs +// fallback has always fed yt-dlp a --load-info-json instead of a URL. +function sourceArgs(url: string, loadInfoJson: string | null): string[] { + return loadInfoJson ? ["--load-info-json", loadInfoJson] : ["--", url]; +} + // Audio-checked mode: we own the audio extraction, so yt-dlp must NOT run // the ExtractAudio postprocessor. We always pass `-c` so resume picks up // where the last validated snapshot left off. @@ -293,6 +317,88 @@ async function runManagedDownload( let fellBackToTranscribe = false; let lastArchiveLine: string | null = null; + // ---------- Attempt 0: metadata prefetch + app-level filters ---------- + // Split the per-video download into a cheap metadata-only pass followed by + // the real download. The metadata lets app-level filters (e.g. skip-live) + // decide before we commit to a full download, and the real attempts reuse it + // via --load-info-json (sourceArgs) so they don't re-extract. Only possible + // when we know the canonical data/<id>/ dir up front; for unidentifiable + // URLs we fall back to the legacy single-call path (positional URL, + // --write-info-json, no prefetch). The audio-check primary deliberately does + // NOT reuse the info json — its long, restart-heavy download would outlive + // yt-dlp's pinned (expirable) format URLs — so it re-extracts even though the + // prefetch ran. + let infoJsonPath: string | null = null; + if (canonicalId && !opts.signal.aborted) { + const videoDir = path.join(channelDir, "data", canonicalId); + const prefetchArgs = [ + "--ignore-config", + "--restrict-filenames", + ...outputArgsForUrl(opts.videoUrl), + "--write-info-json", + "--skip-download", + "--no-write-subs", + "--no-write-auto-subs", + ...channelConfigArgs(opts.channelConfig), + "--", + opts.videoUrl, + ]; + const prefetchRes = await runOneYtdlp(opts, channelDir, prefetchArgs); + const prefetchAvail = attemptSucceeded(prefetchRes.exitCode) + ? undefined + : parseUnavailableFromStderr(prefetchRes.stderrTail); + attempts.push({ + n: 0, + kind: "metadata-prefetch", + handling: opts.channelConfig.handling, + usedCookies: false, + ytdlpExitCode: prefetchRes.exitCode, + availabilityClass: prefetchAvail, + error: attemptSucceeded(prefetchRes.exitCode) + ? undefined + : trimError(prefetchRes.stderrTail), + }); + + const metaPath = path.join(videoDir, "metadata.info.json"); + const metadata = await loadRawMetadata(metaPath); + // Only wire --load-info-json into the real attempts when we actually have + // the metadata file; a failed prefetch falls through to the legacy path so + // the existing auth-retry logic still gets a chance. + if (metadata) infoJsonPath = metaPath; + + const decision = evaluateDownloadFilters({ + metadata, + channelConfig: opts.channelConfig, + settings: { skipLiveDownloads: opts.globalSkipLiveDownloads ?? true }, + }); + if (decision?.skip) { + opts.onLog( + `Skipping ${canonicalId}: ${decision.reason} [filter=${decision.filter}]\n`, + ); + const finishedAt = new Date().toISOString(); + const record: DownloadOutcomeRecord = { + videoId: canonicalId, + webpageUrl: opts.videoUrl, + status: "skipped-filtered", + startedAt, + finishedAt, + attempts, + filter: { name: decision.filter, reason: decision.reason }, + }; + try { + await mkdir(videoDir, { recursive: true }); + await writeDownloadOutcome(videoDir, record); + } catch (err) { + opts.onLog( + `Failed to write download-outcome.json: ${(err as Error).message}\n`, + ); + } + return record; + } + } + // Audio-check re-extracts fresh format URLs, so it never reuses the prefetch. + const reuseInfoJson = infoJsonPath !== null; + // ---------- Attempt 1: primary ---------- const audioCheckEnabled = opts.channelConfig.handling === "transcribe" && @@ -356,14 +462,11 @@ async function runManagedDownload( "--restrict-filenames", ...FULL_LOG_PROGRESS_ARGS, ...outputArgsForUrl(opts.videoUrl), - ...(opts.channelConfig.handling === "youtube" - ? youtubeHandlingArgs(opts.channelConfig) - : transcribeHandlingArgs(opts.channelConfig)), + ...handlingArgs(opts.channelConfig, reuseInfoJson), "--print", `after_video:${ARCHIVE_MARKER} %(extractor)s %(id)s`, ...channelConfigArgs(opts.channelConfig), - "--", - opts.videoUrl, + ...sourceArgs(opts.videoUrl, infoJsonPath), ]; primaryRes = await runOneYtdlp(opts, channelDir, primaryArgs); } @@ -409,14 +512,11 @@ async function runManagedDownload( "--restrict-filenames", ...FULL_LOG_PROGRESS_ARGS, ...outputArgsForUrl(opts.videoUrl), - ...(opts.channelConfig.handling === "youtube" - ? youtubeHandlingArgs(opts.channelConfig) - : transcribeHandlingArgs(opts.channelConfig)), + ...handlingArgs(opts.channelConfig, reuseInfoJson), "--print", `after_video:${ARCHIVE_MARKER} %(extractor)s %(id)s`, ...channelConfigArgs(opts.channelConfig, opts.globalCookiesFromBrowser), - "--", - opts.videoUrl, + ...sourceArgs(opts.videoUrl, infoJsonPath), ]; const retryRes = await runOneYtdlp(opts, channelDir, retryArgs); const retryAvail = attemptSucceeded(retryRes.exitCode) @@ -476,12 +576,14 @@ async function runManagedDownload( "--restrict-filenames", ...FULL_LOG_PROGRESS_ARGS, ...outputArgsForUrl(opts.videoUrl), - ...transcribeHandlingArgs(fallbackConfig), + ...transcribeHandlingArgs(fallbackConfig, true), "--print", `after_video:${ARCHIVE_MARKER} %(extractor)s %(id)s`, ...channelConfigArgs(fallbackConfig, fallbackCookieOverride), - "--load-info-json", - path.join(videoDir, "metadata.info.json"), + ...sourceArgs( + opts.videoUrl, + path.join(videoDir, "metadata.info.json"), + ), ]; const fallbackRes = await runOneYtdlp(opts, channelDir, fallbackArgs); const fallbackAvail = attemptSucceeded(fallbackRes.exitCode) diff --git a/common/ytdlp/runYtdlp.ts b/common/ytdlp/runYtdlp.ts @@ -471,13 +471,15 @@ async function downloadPlaylistManaged( ); } - const { okCount, failedCount, processedCount, firstFailure } = + const { okCount, failedCount, skippedCount, processedCount, firstFailure } = await runManagedDownloads(opts, items, effectiveChannelConfig); if (firstFailure && !opts.signal.aborted) { throw firstFailure; } opts.onLog( - `Managed download complete: ${okCount} succeeded, ${failedCount} failed (${processedCount}/${items.length} processed).\n`, + `Managed download complete: ${okCount} succeeded, ${failedCount} failed` + + (skippedCount ? `, ${skippedCount} skipped by filter` : "") + + ` (${processedCount}/${items.length} processed).\n`, ); await touchLastFullDownload(opts); @@ -487,6 +489,9 @@ async function downloadPlaylistManaged( type ManagedRunResult = { okCount: number; failedCount: number; + // Videos declined by an app-level filter (e.g. skip-live). Neither success + // nor failure: not archived, never aborts the batch. + skippedCount: number; processedCount: number; // Non-null only when abortOnError is on and a non-per-video failure occurred. firstFailure: Error | null; @@ -507,12 +512,14 @@ async function runManagedDownloads( const settings = getSettings(); const globalCookies = settings.cookiesFromBrowser; const inlineTranscribeOnFallback = settings.inlineTranscribeOnFallback; + const globalSkipLiveDownloads = settings.skipLiveDownloads; const sleepSeconds = opts.channelConfig.sleepBetweenDownloadsSeconds ?? settings.sleepBetweenDownloadsSeconds; const limit = pLimit(1); let failedCount = 0; + let skippedCount = 0; const abortOnError = opts.abortOnError !== false; let firstFailure: Error | null = null; let processedCount = 0; @@ -541,11 +548,14 @@ async function runManagedDownloads( globalCookiesFromBrowser: globalCookies || undefined, appendArchive: !opts.ignoreArchive, inlineTranscribeOnFallback, + globalSkipLiveDownloads, }); } finally { task?.end(); } - if (outcome.status === "failed") { + if (outcome.status === "skipped-filtered") { + skippedCount++; + } else if (outcome.status === "failed") { failedCount++; if (abortOnError && !firstFailure) { const lastAttempt = outcome.attempts[outcome.attempts.length - 1]; @@ -580,8 +590,9 @@ async function runManagedDownloads( ); return { - okCount: processedCount - failedCount, + okCount: processedCount - failedCount - skippedCount, failedCount, + skippedCount, processedCount, firstFailure, }; @@ -865,6 +876,7 @@ async function sync(opts: RunYtdlpOpts): Promise<void> { let archive = await readArchive(archivePath); let totalNew = 0; let totalFailed = 0; + let totalSkipped = 0; let firstFailure: Error | null = null; for ( @@ -895,6 +907,7 @@ async function sync(opts: RunYtdlpOpts): Promise<void> { const res = await runManagedDownloads(opts, newUrls, opts.channelConfig); totalNew += res.okCount; totalFailed += res.failedCount; + totalSkipped += res.skippedCount; if (res.firstFailure) { firstFailure = res.firstFailure; break; @@ -911,7 +924,9 @@ async function sync(opts: RunYtdlpOpts): Promise<void> { if (firstFailure && !opts.signal.aborted) throw firstFailure; opts.onLog( - `Sync complete: ${totalNew} new downloaded, ${totalFailed} failed.\n`, + `Sync complete: ${totalNew} new downloaded, ${totalFailed} failed` + + (totalSkipped ? `, ${totalSkipped} skipped by filter` : "") + + `.\n`, ); await touchLastSync(opts); await safeBackfillAvailability(opts); diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] +- **Global default for parallel transcriptions.** A new **Settings → Parallel transcriptions** field sets how many videos a "Transcribe missing" / bucket run transcribes at once when its per-run Concurrency input is left blank (default **2**, clamped 1–16). This replaces the old `PARALLEL_TRANSCRIBE_LIMIT` env-var default of 4 as the source of the default: the value is now persisted in `settings.json`, shown as the placeholder in each channel's Concurrency input, and used as the server-side fallback. The per-run Concurrency input still overrides it for a single run. +- **Managed downloads skip live and upcoming videos by default.** Before downloading each video, the managed downloader now runs a quick metadata-only pass, then evaluates an app-level filter: videos that are currently live or scheduled/upcoming are skipped (their finished VODs still download normally — a skip isn't archived, so the next **Sync** / **Download missing** retries the video once the stream ends). A skip is recorded in the video's `download-outcome.json` (`status: "skipped-filtered"` with the filter name and reason) and logged to `download.log`; it never counts as a failure or aborts the batch, and the run's summary line reports how many were skipped. Skipped-live videos are surfaced in a new **Skipped: live or upcoming** bucket in the channel's Diagnostics (with a **Retry** to force an attempt now). On by default for every channel via a new **Settings → Skip live and upcoming videos** toggle, with a per-channel override (`config.json` `skipLiveDownloads`). To support the filter (and as a reusable building block for future per-video decisions), each managed download is now split into a metadata fetch followed by the real download, which reuses that metadata via `--load-info-json` so the second pass doesn't re-extract; the audio-integrity-checked download path re-extracts as before. Turn the toggle off for a channel that streams nothing to allow live captures. - **Global default social links, overridable per site.** Footer social links can now be defined once in **Settings → Social links** and apply to every site by default. Each site's form gained a **Use global default social links** checkbox (on by default): leave it checked to inherit the global list, or uncheck it to give that site its own links (an empty list shows none). Existing sites keep their current footer — sites that already had links stay as explicit overrides until you flip the toggle. Validation and SVG sanitization are unchanged and shared by both the global and per-site editors. - **Import a single off-playlist video by URL.** A channel's **Playlist** stage gained an **Import single video** panel: paste a video URL, click **Import video**, and that one video is downloaded into the channel using its normal handling (subtitles + metadata for YouTube channels, audio for transcribe channels) and appended to the archive — no need to add it to the stored playlist first. It reuses the same single-video downloader as the per-video "Download" button (including the auth-retry and no-subs→audio fallbacks), streams yt-dlp's output live, and runs on the channel's download queue. Transcription stays a separate step (use the per-video **Transcribe** button afterward), same as a normal download. - **Duplicate shorts detection on `/actionable`.** A new **Duplicate shorts** section finds the same short re-uploaded under a new id, re-titled, posted on another channel, or cross-posted to another platform. It runs a global, cross-platform pass: a cheap duration pre-cluster narrows candidates, then transcript content is compared (exact text hash → near-duplicate similarity → containment for "this short is a clip of a longer video"). Matches are **content-confirmed only** — a shared duration alone never clusters videos (that produced huge false clusters of unrelated same-length videos), and plain (non-karaoke) VTT captions that the index stores as zero cues are recovered by reading the raw transcript. Pick a scope — **Shorts only** (default ≤ 180s) or **All durations** (a heavier one-off that also surfaces clip-of-longer matches) — and click **Detect duplicates**; results list each cluster with its members (links), match strength, score, and cross-platform/cross-channel/clip-of-longer badges. It's **flag-only** — nothing is merged or deleted. Reads the cues + per-video stats written by **Build index** + **Build stats dataset**, so run those first; the report is written to `transcripts/duplicates.json` (also producible from the CLI via `pnpm --filter export run detect:duplicates [-- --all-durations]`). diff --git a/editor/app/channels/[slug]/components/stages/DiagnosticsStage.tsx b/editor/app/channels/[slug]/components/stages/DiagnosticsStage.tsx @@ -44,6 +44,7 @@ type Props = { missingFromArchiveIds: string[]; duplicateDirIds: string[]; nonStandardVttIds: string[]; + skippedByFilterIds: string[]; totals: { videos: number; transcribed: number; downloaded: number }; availability: AvailabilitySnapshot; availabilityShard: ShardConfigSummary | null; @@ -58,6 +59,7 @@ export function DiagnosticsStage({ missingFromArchiveIds, duplicateDirIds, nonStandardVttIds, + skippedByFilterIds, totals, availability, availabilityShard, @@ -95,6 +97,14 @@ export function DiagnosticsStage({ "Multiple directories resolve to the same video ID (e.g., legacy names).", ariaLabel: "duplicate directories", }, + { + ids: skippedByFilterIds, + label: "Skipped: live or upcoming", + description: + "Declined by the skip-live filter because the video was currently live or scheduled/upcoming. A normal sync retries these automatically once the stream ends; use Retry to force a download attempt now.", + ariaLabel: "skipped live", + retry: true, + }, ]; const populated = buckets.filter((b) => b.ids.length > 0); const total = populated.reduce((acc, b) => acc + b.ids.length, 0); diff --git a/editor/app/channels/[slug]/lib/stageStatus.ts b/editor/app/channels/[slug]/lib/stageStatus.ts @@ -25,6 +25,7 @@ export function normalizeBuckets( duplicateDirs: raw?.duplicateDirs ?? [], partialDownloads: raw?.partialDownloads ?? [], nonStandardVtt: raw?.nonStandardVtt ?? [], + skippedByFilter: raw?.skippedByFilter ?? [], }; } diff --git a/editor/app/channels/[slug]/page.tsx b/editor/app/channels/[slug]/page.tsx @@ -21,6 +21,7 @@ import { } from "yt-dlp-transcript-common/controller/shard"; import { loadDownloadOutcome } from "yt-dlp-transcript-common/lib/downloadOutcome-server"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; +import { getSettings } from "yt-dlp-transcript-common/lib/settings"; import { resolvePrimaryVtt } from "yt-dlp-transcript-common/lib/videoStatus"; import { platformQueueKey, @@ -123,6 +124,7 @@ export default async function ChannelDetailPage({ const filter = parseFilter(rawFilter); const queryRaw = typeof sp.q === "string" ? sp.q : ""; const paths = getPaths(); + const settings = getSettings(); const config = await readChannelConfig(paths, slug); if (!config) notFound(); @@ -244,7 +246,7 @@ export default async function ChannelDetailPage({ existingQueues={existingQueues} failedVideoIds={failedVideoIds} downloadedNoTranscriptIds={actionableDownloadedNoTranscriptIds} - defaultConcurrency={paths.parallelTranscribeLimit} + defaultConcurrency={settings.parallelTranscriptions} defaultQueueKey={TRANSCRIPTION_QUEUE} missingShard={transcribeMissingShard} /> @@ -267,6 +269,7 @@ export default async function ChannelDetailPage({ missingFromArchiveIds={buckets.missingFromArchive} duplicateDirIds={buckets.duplicateDirs} nonStandardVttIds={buckets.nonStandardVtt} + skippedByFilterIds={buckets.skippedByFilter} totals={snapshot.totals} availability={availability} availabilityShard={availabilityShard} @@ -397,7 +400,7 @@ export default async function ChannelDetailPage({ defaultTranscribeQueue={TRANSCRIPTION_QUEUE} defaultDownloadQueue={platformDefaultQueueKey} existingQueues={existingQueues} - defaultConcurrency={paths.parallelTranscribeLimit} + defaultConcurrency={settings.parallelTranscriptions} /> } right={ diff --git a/editor/app/channels/[slug]/pipelineActions.ts b/editor/app/channels/[slug]/pipelineActions.ts @@ -258,6 +258,7 @@ export async function importVideoAction( signal, globalCookiesFromBrowser: settings.cookiesFromBrowser || undefined, inlineTranscribeOnFallback: settings.inlineTranscribeOnFallback, + globalSkipLiveDownloads: settings.skipLiveDownloads, appendArchive: true, }); if (videoId) { diff --git a/editor/app/channels/[slug]/videos/[id]/videoActions.ts b/editor/app/channels/[slug]/videos/[id]/videoActions.ts @@ -165,6 +165,7 @@ export async function downloadVideoPipelineAction( signal, globalCookiesFromBrowser: settings.cookiesFromBrowser || undefined, inlineTranscribeOnFallback: settings.inlineTranscribeOnFallback, + globalSkipLiveDownloads: settings.skipLiveDownloads, appendArchive: true, }); revalidatePath(`/channels/${slug}/videos/${videoId}`); diff --git a/editor/app/settings/actions.ts b/editor/app/settings/actions.ts @@ -4,6 +4,7 @@ import { revalidatePath } from "next/cache"; import { normalizeSocialSvg, parseSocialLinks, + PARALLEL_TRANSCRIPTIONS_MAX, SLEEP_BETWEEN_DOWNLOADS_MAX_SECONDS, TRANSCRIPT_PAGE_HARD_CAP_BYTES, TRANSCRIPT_PAGE_MIN_BYTES, @@ -29,8 +30,12 @@ export async function saveSettingsAction( const sleepRaw = String( formData.get("sleepBetweenDownloadsSeconds") ?? "", ).trim(); + const parallelRaw = String( + formData.get("parallelTranscriptions") ?? "", + ).trim(); const inlineTranscribeOnFallback = formData.get("inlineTranscribeOnFallback") === "on"; + const skipLiveDownloads = formData.get("skipLiveDownloads") === "on"; const transcribeArgsRaw = String(formData.get("transcribeArgs") ?? ""); const transcribeArgs = transcribeArgsRaw .split("\n") @@ -76,6 +81,17 @@ export async function saveSettingsAction( }; } + const parallelParsed = Number.parseInt(parallelRaw, 10); + if (!Number.isFinite(parallelParsed)) { + return { ok: false, error: "parallelTranscriptions must be a number" }; + } + if (parallelParsed < 1 || parallelParsed > PARALLEL_TRANSCRIPTIONS_MAX) { + return { + ok: false, + error: `parallelTranscriptions must be between 1 and ${PARALLEL_TRANSCRIPTIONS_MAX}`, + }; + } + let socialInput: unknown; try { socialInput = JSON.parse(String(formData.get("socialLinksJson") ?? "[]")); @@ -110,7 +126,9 @@ export async function saveSettingsAction( transcribeArgs, cookiesFromBrowser, sleepBetweenDownloadsSeconds: sleepParsed, + parallelTranscriptions: parallelParsed, inlineTranscribeOnFallback, + skipLiveDownloads, socialLinks, }; await writeSettings(next); diff --git a/editor/app/settings/components/SettingsForm.tsx b/editor/app/settings/components/SettingsForm.tsx @@ -94,6 +94,13 @@ export function SettingsForm({ initial }: Props) { type="number" hint="Pause inserted between per-video yt-dlp invocations in managed batch downloads (download-from-playlist, download-missing). 0 disables. Default 10s. Each channel can override this in its Advanced settings." /> + <Field + label="Parallel transcriptions" + name="parallelTranscriptions" + defaultValue={String(initial.parallelTranscriptions)} + type="number" + hint="Default number of videos transcribed at once when a run doesn't set its own concurrency (1–16). Default 2. Higher values use more CPU/RAM. The per-run Concurrency input on a channel overrides this for a single run." + /> <label className="flex items-start gap-2 text-sm"> <input type="checkbox" @@ -114,6 +121,27 @@ export function SettingsForm({ initial }: Props) { </span> </span> </label> + <label className="flex items-start gap-2 text-sm"> + <input + type="checkbox" + name="skipLiveDownloads" + defaultChecked={initial.skipLiveDownloads} + className="mt-1" + /> + <span className="flex flex-col gap-1"> + <span className="font-medium"> + Skip live and upcoming videos + </span> + <span className="text-xs text-zinc-500"> + Before each managed download, a quick metadata-only pass checks + whether the video is currently live or scheduled/upcoming and, if + so, skips it (finished livestream VODs download normally). The skip + is recorded but not archived, so the next sync retries the video + once the stream ends. On by default. Each channel can override this + in its Advanced settings. + </span> + </span> + </label> <fieldset className="flex flex-col gap-3 border border-zinc-200 dark:border-zinc-800 rounded p-3"> <legend className="px-1 text-sm font-medium">Social links</legend> <p className="text-xs text-zinc-500"> diff --git a/editor/app/settings/page.tsx b/editor/app/settings/page.tsx @@ -21,7 +21,6 @@ export default function SettingsPage() { ["ytdlpBin", paths.ytdlpBin], ["whisperBin", paths.whisperBin], ["whisperModel", paths.whisperModel], - ["parallelTranscribeLimit", String(paths.parallelTranscribeLimit)], ]; return ( @@ -47,8 +46,8 @@ export default function SettingsPage() { <p className="text-sm text-zinc-500 mt-2"> Read-only view of <code>getPaths()</code>. Set the matching env vars (TRANSCRIPTS_DIR, SETTINGS_FILE, EXPORT_PUBLIC_DIR, YTDLP_BIN, - WHISPER_BIN, WHISPER_MODEL, PARALLEL_TRANSCRIBE_LIMIT) before - launching to override. The transcription command above takes + WHISPER_BIN, WHISPER_MODEL) before launching to override. The + transcription command above takes precedence over <code>whisperBin</code> / <code>whisperModel</code>{" "} when running channel transcribe actions. </p> diff --git a/editor/e2e/fixtures/bin/fake-ytdlp.mjs b/editor/e2e/fixtures/bin/fake-ytdlp.mjs @@ -85,12 +85,36 @@ async function writeMetadata(videoDir, id, opts = {}) { meta.subtitles = {}; meta.automatic_captions = {}; } + // Live-status sentinels for the skip-live filter tests. is_live = streaming + // now, is_upcoming = scheduled, was_live = finished VOD (downloads normally). + if (opts.isLive) { + meta.is_live = true; + meta.live_status = "is_live"; + } else if (opts.isUpcoming) { + meta.live_status = "is_upcoming"; + } else if (opts.wasLive) { + meta.was_live = true; + meta.live_status = "was_live"; + } await writeFile( path.join(videoDir, "metadata.info.json"), JSON.stringify(meta), ); } +// Sentinels encoded in a video id/URL select metadata variants so a fixture +// can deterministically exercise each filter/fallback branch. +function urlSentinels(url) { + const lower = (url ?? "").toLowerCase(); + return { + livechatOnly: lower.includes("livechatonly"), + noCaptions: lower.includes("nocaptions"), + isLive: lower.includes("islive"), + isUpcoming: lower.includes("isupcoming"), + wasLive: lower.includes("waslive"), + }; +} + async function writeTranscript(videoDir) { const vtt = `WEBVTT\n\n00:00:00.000 --> 00:00:05.000\nFake transcript line one.\n\n00:00:05.000 --> 00:00:10.000\nFake transcript line two.\n`; await writeFile(path.join(videoDir, "transcript.en.vtt"), vtt); @@ -480,14 +504,51 @@ async function main() { return; } + // Metadata prefetch: --write-info-json --skip-download with subtitles + // explicitly disabled and a positional URL. Writes only metadata.info.json + // (honoring live sentinels) so the managed downloader's app-level filters + // can decide before the real download. + if ( + has("--skip-download") && + has("--write-info-json") && + !has("--write-auto-subs") && + !has("--flat-playlist") + ) { + const url = lastNonFlag(); + if (!url) { + process.stderr.write(`[fake-ytdlp] prefetch mode missing URL\n`); + process.exit(2); + } + const id = urlIdYouTube(url); + const videoDir = path.join("data", id); + await ensureDir(videoDir); + await appendFile("fake-ytdlp.invocations", `prefetch:${url}\n`); + await writeMetadata(videoDir, id, urlSentinels(url)); + process.stdout.write(`[fake-ytdlp] metadata prefetch ${id}\n`); + return; + } + // Managed YouTube-handling per-URL invocation: --skip-download with - // --write-auto-subs, no -a, last positional is the URL. + // --write-auto-subs, no -a. The real download reuses the prefetched + // metadata via --load-info-json (no positional URL) — derive the id from + // the info.json path; otherwise the URL is the last positional. if ( has("--skip-download") && has("--write-auto-subs") && !has("--flat-playlist") ) { - const url = lastNonFlag(); + const infoJsonPath = arg("--load-info-json"); + let url; + if (infoJsonPath) { + const id = path.basename(path.dirname(infoJsonPath)); + url = `https://www.youtube.com/watch?v=${id}`; + await appendFile( + "fake-ytdlp.invocations", + `load-info-json:${infoJsonPath}\n`, + ); + } else { + url = lastNonFlag(); + } if (!url) { process.stderr.write( `[fake-ytdlp] youtube single-url mode missing URL\n`, diff --git a/editor/e2e/fixtures/test-transcripts/skip-live-channel/channels/test-live/config.json b/editor/e2e/fixtures/test-transcripts/skip-live-channel/channels/test-live/config.json @@ -0,0 +1,5 @@ +{ + "handling": "youtube", + "name": "Test Skip Live", + "url": "https://www.youtube.com/@example/videos" +} diff --git a/editor/e2e/fixtures/test-transcripts/skip-live-channel/channels/test-live/playlist b/editor/e2e/fixtures/test-transcripts/skip-live-channel/channels/test-live/playlist @@ -0,0 +1,4 @@ +https://www.youtube.com/watch?v=islive000001 +https://www.youtube.com/watch?v=isupcoming01 +https://www.youtube.com/watch?v=waslive00001 +https://www.youtube.com/watch?v=normalvid001 diff --git a/editor/e2e/settings.spec.ts b/editor/e2e/settings.spec.ts @@ -56,6 +56,36 @@ test("saves global default social links", async ({ page }) => { expect(saved.socialLinks?.[0].svg).toContain("currentColor"); }); +test("defaults parallel transcriptions to 2 and persists a new value", async ({ + page, +}) => { + await page.goto("/settings"); + await expect(page.getByLabel(/parallel transcriptions/i)).toHaveValue("2"); + + await page.getByLabel(/parallel transcriptions/i).fill("3"); + await page.getByRole("button", { name: /save settings/i }).click(); + await expect( + page.getByRole("status").filter({ hasText: "Saved" }), + ).toBeVisible(); + + const saved = await readJson<{ parallelTranscriptions?: number }>( + "test-settings.json", + ); + expect(saved.parallelTranscriptions).toBe(3); + + await page.reload(); + await expect(page.getByLabel(/parallel transcriptions/i)).toHaveValue("3"); +}); + +test("rejects out-of-range parallel transcriptions", async ({ page }) => { + await page.goto("/settings"); + await page.getByLabel(/parallel transcriptions/i).fill("99"); + await page.getByRole("button", { name: /save settings/i }).click(); + await expect(page.locator("form").getByRole("alert")).toContainText( + "between", + ); +}); + test("rejects out-of-range maxTranscriptPageBytes", async ({ page }) => { await page.goto("/settings"); await page.getByLabel(/max transcript page bytes/i).fill("42"); diff --git a/editor/e2e/skip-live.spec.ts b/editor/e2e/skip-live.spec.ts @@ -0,0 +1,167 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; +import { test, expect } from "@playwright/test"; +import { + pathExists, + readJson, + resetData, + resolvePath, + writeSettings, +} from "./helpers"; + +const CHANNEL = "test-live"; +const ROOT = `test-transcripts/channels/${CHANNEL}`; +const here = path.dirname(fileURLToPath(import.meta.url)); +const baseUrl = process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:3011"; + +type DownloadOutcome = { + status: string; + attempts: Array<{ kind: string; handling: string }>; + filter?: { name: string; reason: string }; +}; + +type Snapshot = { + buckets: { skippedByFilter?: string[] }; +}; + +async function defaultSettings(): Promise<Record<string, unknown>> { + const raw = await readFile( + path.join(here, "fixtures", "test-settings.default.json"), + "utf8", + ); + return JSON.parse(raw) as Record<string, unknown>; +} + +async function readInvocations(): Promise<string> { + return readFile( + resolvePath(`${ROOT}/fake-ytdlp.invocations`), + "utf8", + ).catch(() => ""); +} + +async function readArchive(): Promise<string> { + return readFile(resolvePath(`${ROOT}/archive`), "utf8").catch(() => ""); +} + +test("skip-live on by default: live + upcoming skipped, VOD + normal download", async ({ + page, +}) => { + test.setTimeout(120_000); + await resetData("skip-live-channel"); + await page.goto(`/channels/${CHANNEL}`); + await page.getByRole("button", { name: "Download videos" }).click(); + const log = page.getByLabel("Download videos output"); + + await expect(log).toContainText( + "Skipping islive000001: video is currently live [filter=skipLive]", + { timeout: 60_000 }, + ); + await expect(log).toContainText( + "Skipping isupcoming01: video is an upcoming/scheduled livestream [filter=skipLive]", + { timeout: 60_000 }, + ); + await expect(log).toContainText("2 skipped by filter", { timeout: 60_000 }); + await expect(log).toContainText("Managed download complete", { + timeout: 60_000, + }); + + // Live video: recorded as skipped, no transcript, not archived. + const liveOutcome = await readJson<DownloadOutcome>( + `${ROOT}/data/islive000001/download-outcome.json`, + ); + expect(liveOutcome.status).toBe("skipped-filtered"); + expect(liveOutcome.filter?.name).toBe("skipLive"); + expect( + liveOutcome.attempts.some((a) => a.kind === "metadata-prefetch"), + ).toBe(true); + expect( + await pathExists(`${ROOT}/data/islive000001/transcript.en.vtt`), + ).toBe(false); + expect(await readArchive()).not.toContain("islive000001"); + + // Finished livestream VOD must download normally (regression guard). + expect( + await pathExists(`${ROOT}/data/waslive00001/transcript.en.vtt`), + ).toBe(true); + const vodOutcome = await readJson<DownloadOutcome>( + `${ROOT}/data/waslive00001/download-outcome.json`, + ); + expect(vodOutcome.status).toBe("ok"); + expect(await readArchive()).toContain("waslive00001"); + + // Normal video downloads, and the split actually happened (prefetch + reuse). + expect( + await pathExists(`${ROOT}/data/normalvid001/transcript.en.vtt`), + ).toBe(true); + const invocations = await readInvocations(); + expect(invocations).toMatch(/prefetch:.*normalvid001/); + expect(invocations).toMatch( + /load-info-json:.*data\/normalvid001\/metadata\.info\.json/, + ); + + // Snapshot surfaces the skipped-live videos in their own bucket. + const snapshot = await readJson<Snapshot>(`${ROOT}/snapshot.json`); + expect(snapshot.buckets.skippedByFilter ?? []).toEqual( + expect.arrayContaining(["islive000001", "isupcoming01"]), + ); + expect(snapshot.buckets.skippedByFilter ?? []).not.toContain("waslive00001"); +}); + +test("global skipLiveDownloads=false downloads live videos (split still runs)", async ({ + page, +}) => { + test.setTimeout(120_000); + await resetData("skip-live-channel"); + await writeSettings({ + ...(await defaultSettings()), + skipLiveDownloads: false, + }); + await page.goto(`/channels/${CHANNEL}`); + await page.getByRole("button", { name: "Download videos" }).click(); + const log = page.getByLabel("Download videos output"); + await expect(log).toContainText("Managed download complete", { + timeout: 60_000, + }); + await expect(log).not.toContainText("Skipping islive000001"); + + // With the filter off, the live video downloads normally... + expect( + await pathExists(`${ROOT}/data/islive000001/transcript.en.vtt`), + ).toBe(true); + // ...but the metadata/download split still runs (always-on primitive). + expect(await readInvocations()).toMatch(/prefetch:.*islive000001/); +}); + +test("per-channel skipLiveDownloads=false overrides the global default", async ({ + page, +}) => { + test.setTimeout(120_000); + await resetData("skip-live-channel"); + // Global default stays on; the channel opts out. + await writeFile( + resolvePath(`${ROOT}/config.json`), + JSON.stringify( + { + handling: "youtube", + name: "Test Skip Live", + url: "https://www.youtube.com/@example/videos", + skipLiveDownloads: false, + }, + null, + 2, + ), + ); + await fetch(`${baseUrl}/api/test/invalidate-cache`).catch(() => {}); + + await page.goto(`/channels/${CHANNEL}`); + await page.getByRole("button", { name: "Download videos" }).click(); + const log = page.getByLabel("Download videos output"); + await expect(log).toContainText("Managed download complete", { + timeout: 60_000, + }); + await expect(log).not.toContainText("Skipping islive000001"); + expect( + await pathExists(`${ROOT}/data/islive000001/transcript.en.vtt`), + ).toBe(true); +}); diff --git a/export/public/duplicates.json b/export/public/duplicates.json @@ -1 +1 @@ -{"version":1,"generatedAt":"2026-06-05T02:48:49.807Z","runConfig":{"thresholdSeconds":180,"durationToleranceSeconds":2,"nearThreshold":0.6,"containmentThreshold":0.8,"shingleSize":5},"totals":{"videosScanned":34915,"clusters":97,"videosInClusters":200},"clusters":[{"clusterId":"a69e01ae87f9db05eac6e423cf12326725fb57a5","matchKind":"transcript-exact","score":1,"contained":false,"durationBucket":57,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/82ie0HA0wtg","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"82ie0HA0wtg","title":"Woke Chic Destroyed","duration":57,"uploadDate":"20250529","hasTranscript":true},{"slug":"jeremy-hambly/nseztjWe248","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"nseztjWe248","title":"Woke Chic Embarrassed With Logic","duration":57,"uploadDate":"20250521","hasTranscript":true},{"slug":"the-quartering/cNW4q0XoNRw","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"cNW4q0XoNRw","title":"Woke Chic Destroyed","duration":57,"uploadDate":"20241027","hasTranscript":true},{"slug":"the-quartering/f7Ya6IPjpak","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"f7Ya6IPjpak","title":"Woke Chic Embarrassed With Logic","duration":57,"uploadDate":"20241029","hasTranscript":true}]},{"clusterId":"07c4f72c74efea57425b089b29ea2d5a0e06ffcb","matchKind":"transcript-exact","score":1,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/lib3mImlH98","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"lib3mImlH98","title":"Woke Reporter Embarassed!","duration":60,"uploadDate":"20250509","hasTranscript":true},{"slug":"jeremy-hambly/td_KJ1hZ5ew","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"td_KJ1hZ5ew","title":"Woke Reporter Embarassed!","duration":60,"uploadDate":"20250508","hasTranscript":true},{"slug":"the-quartering/tm4Oa_276qA","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"tm4Oa_276qA","title":"Woke Reporter Embarassed!","duration":59,"uploadDate":"20240825","hasTranscript":true}]},{"clusterId":"ad46d712f5d14a9fcf9a82abb3f0d3ebf9c139ea","matchKind":"transcript-exact","score":1,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/GcRzOHpoQwo","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"GcRzOHpoQwo","title":"Woke College Protesters EPICLY Trolled! Her Brain Shuts Down!","duration":59,"uploadDate":"20250512","hasTranscript":true},{"slug":"jeremy-hambly/VNd9bCNrVB0","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"VNd9bCNrVB0","title":"Woke College Protesters EPICLY Trolled! Her Brain Shuts Down!","duration":59,"uploadDate":"20250516","hasTranscript":true},{"slug":"the-quartering/yWCadzdol7Q","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"yWCadzdol7Q","title":"Woke College Protesters EPICLY Trolled! Her Brain Shuts Down!","duration":59,"uploadDate":"20240509","hasTranscript":true}]},{"clusterId":"ae65348b17aa0e60724cf8460e66badc08cd7167","matchKind":"transcript-exact","score":1,"contained":false,"durationBucket":71,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"clawstruck/ta9rjd3xHuI","channelSlug":"clawstruck","channel":"ClawStruck","platform":"youtube","id":"ta9rjd3xHuI","title":"Giving Away A Ton Of Cash & Support To You...","duration":71,"uploadDate":"20200421","hasTranscript":true},{"slug":"exclusively-games/lruthazfD2A","channelSlug":"exclusively-games","channel":"Exclusively Games","platform":"youtube","id":"lruthazfD2A","title":"Giving Away A Ton Of Cash & Support To You...","duration":71,"uploadDate":"20200419","hasTranscript":true},{"slug":"unsleeved-media/RwYeqYmP4zI","channelSlug":"unsleeved-media","channel":"UnsleevedMedia","platform":"youtube","id":"RwYeqYmP4zI","title":"Giving Away A Ton Of Cash & Support To You...","duration":71,"uploadDate":"20200419","hasTranscript":true}]},{"clusterId":"22685f208013a53b4fff87a0fff9ec2ccaf1114b","matchKind":"transcript-exact","score":1,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":false,"videoRefs":[{"slug":"the-quartering/_wvKNkcWfG0","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"_wvKNkcWfG0","title":"Woman RAGES Over Having To Work","duration":59,"uploadDate":"20240223","hasTranscript":true},{"slug":"the-quartering/0ick8ms9yzs","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"0ick8ms9yzs","title":"Woman Has MELTDOWN Over Having To Work","duration":59,"uploadDate":"20240228","hasTranscript":true}]},{"clusterId":"445cd3d8b20ac458af4c168e3db14f68755c8b60","matchKind":"transcript-exact","score":1,"contained":false,"durationBucket":50,"crossPlatform":false,"crossChannel":false,"videoRefs":[{"slug":"the-quartering/jCo14Y0gNQk","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"jCo14Y0gNQk","title":"Literal Psycho Behavior!","duration":50,"uploadDate":"20240218","hasTranscript":true},{"slug":"the-quartering/YrAa1m4ATOU","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"YrAa1m4ATOU","title":"She Ruined A Mans Life!","duration":50,"uploadDate":"20240220","hasTranscript":true}]},{"clusterId":"6b8cd61216c8bf26eb7538266b5a314ff2109277","matchKind":"transcript-exact","score":1,"contained":false,"durationBucket":62,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/1P_a_7S45dY","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"1P_a_7S45dY","title":"Big News! TheQuartering, Melonie Mac, & Styxhexenhammer & Prizes For You! I Jeremy Hambly","duration":62,"uploadDate":"20240621","hasTranscript":true},{"slug":"the-quartering/hAsBQCwux2A","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"hAsBQCwux2A","title":"Big News! Melonie Mac, Styxhexenhammer, TheQuartering & Prizes For You!","duration":62,"uploadDate":"20240621","hasTranscript":true}]},{"clusterId":"b096c2c6724ec79fe744636eded944dc12f649aa","matchKind":"transcript-exact","score":1,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":false,"videoRefs":[{"slug":"the-quartering/Iwv-fJPIGMY","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"Iwv-fJPIGMY","title":"A Man DARED To By Her A Drink...","duration":59,"uploadDate":"20240228","hasTranscript":true},{"slug":"the-quartering/P_pIopfDQM8","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"P_pIopfDQM8","title":"She Hates Most Men (Is Single Of Course)","duration":59,"uploadDate":"20240223","hasTranscript":true}]},{"clusterId":"fe2dc9420ae03e1cc08ff5d0cc1cfb68b50c04d6","matchKind":"transcript-near","score":0.851,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/0TxvFJ3Ebrk","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"0TxvFJ3Ebrk","title":"Woke Dude DESPERATE For Attention","duration":60,"uploadDate":"20250521","hasTranscript":true},{"slug":"the-quartering/O2Tzm35TbBY","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"O2Tzm35TbBY","title":"Woke Dude DESPERATE For Attention","duration":60,"uploadDate":"20240906","hasTranscript":true},{"slug":"the-quartering/pudRemcY9c4","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"pudRemcY9c4","title":"Woke Creep DESTORYED By Facts & Logic","duration":59,"uploadDate":"20240804","hasTranscript":true}]},{"clusterId":"aeee72d67d78a4babb3d36ee28f6d5e3b2d5990f","matchKind":"transcript-near","score":0.992,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/xGb3ohDbrcU","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"xGb3ohDbrcU","title":"Woke Creep Destroyed In Woman's Locker Room","duration":59,"uploadDate":"20250518","hasTranscript":true},{"slug":"the-quartering/IecBIvnsudY","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"IecBIvnsudY","title":"Woke Creep Destroyed In Woman's Locker Room","duration":59,"uploadDate":"20240710","hasTranscript":true}]},{"clusterId":"b3a9fc0b2b2ec3765aee6bd359d3a9a2e36ba4dd","matchKind":"transcript-near","score":0.952,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/AKUmnn1aqIs","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"AKUmnn1aqIs","title":"Gen Z Women Calls All Men Creeps","duration":59,"uploadDate":"20250604","hasTranscript":true},{"slug":"the-quartering/ONGeoBABQ5k","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"ONGeoBABQ5k","title":"Gen Z Women Calls All Men Creeps","duration":59,"uploadDate":"20241001","hasTranscript":true}]},{"clusterId":"de3fc8a4835186cbc3e85a8ee05c33faa1fef059","matchKind":"transcript-near","score":0.951,"contained":false,"durationBucket":60,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/Xik882z6zPU","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"Xik882z6zPU","title":"Woke Gen Z Soyboy WAKES Up & Supports Trump","duration":60,"uploadDate":"20250524","hasTranscript":true},{"slug":"the-quartering/oeGS9z244co","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"oeGS9z244co","title":"Woke Gen Z Soyboy WAKES Up & Supports Trump","duration":60,"uploadDate":"20240906","hasTranscript":true}]},{"clusterId":"6512ed43753af89aaea107e9054c7f9d9b3000c8","matchKind":"transcript-near","score":0.947,"contained":false,"durationBucket":60,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/fYXfQuceF6Q","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"fYXfQuceF6Q","title":"Woke Democrats OWNED As Alexandria Ocasio Cortez HUMILATED By Donald Trump","duration":60,"uploadDate":"20250506","hasTranscript":true},{"slug":"the-quartering/4HfvHoOD1qI","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"4HfvHoOD1qI","title":"Woke Democrats OWNED As Alexandria Ocasio-Cortez HUMILATED By Donald Trump","duration":60,"uploadDate":"20240525","hasTranscript":true}]},{"clusterId":"890e7c32f8f4be4f806bcf9e8050c78501246c63","matchKind":"transcript-near","score":0.941,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/b-JbaicaBlU","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"b-JbaicaBlU","title":"Behold Pure Woke Insanity","duration":59,"uploadDate":"20250604","hasTranscript":true},{"slug":"the-quartering/L7yzplC3tEo","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"L7yzplC3tEo","title":"Behold Pure Woke Insanity","duration":59,"uploadDate":"20221118","hasTranscript":true}]},{"clusterId":"a5e813c33e7fa7d85b68fd64cab2b384504dea90","matchKind":"transcript-near","score":0.935,"contained":false,"durationBucket":60,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/4G-qijDSvBs","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"4G-qijDSvBs","title":"Woke College Protesters Crushed By Based Girl","duration":60,"uploadDate":"20250525","hasTranscript":true},{"slug":"the-quartering/K4Xk8UMvczk","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"K4Xk8UMvczk","title":"Woke College Protesters Crushed By Based Girl","duration":60,"uploadDate":"20240522","hasTranscript":true}]},{"clusterId":"acfc87dc6e25239958289f5a608f2f34e2daa8bf","matchKind":"transcript-near","score":0.934,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/6u-x6tf4weQ","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"6u-x6tf4weQ","title":"Unhinged Woke Woman Says Whites Need Permission To Talk","duration":59,"uploadDate":"20250616","hasTranscript":true},{"slug":"the-quartering/OTp_VLb-XDo","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"OTp_VLb-XDo","title":"Unhinged Woke Woman Says Whites Need Permission To Talk","duration":59,"uploadDate":"20240615","hasTranscript":true}]},{"clusterId":"20ba1df56ac3cb53f277561faa56bd2e4989da0d","matchKind":"transcript-near","score":0.932,"contained":false,"durationBucket":60,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/oG8ywvrgJeY","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"oG8ywvrgJeY","title":"Woke Man Kicked From Woman's Room #news #wokeness","duration":60,"uploadDate":"20250531","hasTranscript":true},{"slug":"the-quartering/nvvyNrPqhW0","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"nvvyNrPqhW0","title":"Woke Man Kicked From Woman's Room #news #wokeness","duration":60,"uploadDate":"20240702","hasTranscript":true}]},{"clusterId":"a179afa0cc8a22ded1b1fc0ae38af97a3f2d829f","matchKind":"transcript-near","score":0.929,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/vWcmnxjmPPk","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"vWcmnxjmPPk","title":"Donald Trump Responds To CIA Admitting To BOMBSHELL Lies! They Hid Info From Him!","duration":59,"uploadDate":"20250521","hasTranscript":true},{"slug":"the-quartering/KWI6VciUGSQ","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"KWI6VciUGSQ","title":"Donald Trump Responds To CIA Admitting To BOMBSHELL Lies! They Hid Info From Him!","duration":59,"uploadDate":"20240502","hasTranscript":true}]},{"clusterId":"73ce1702127d892def7a7e55fcfb9ba4a7e1c162","matchKind":"transcript-near","score":0.926,"contained":false,"durationBucket":53,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/zrQOnXnx4Fg","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"zrQOnXnx4Fg","title":"Run For Your LIFE If You See Him!","duration":53,"uploadDate":"20250602","hasTranscript":true},{"slug":"the-quartering/_bvfxMvguSI","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"_bvfxMvguSI","title":"Run For Your LIFE If You See Him!","duration":53,"uploadDate":"20230228","hasTranscript":true}]},{"clusterId":"64751da884e91714624668e196a9f8bfa35408a4","matchKind":"transcript-near","score":0.901,"contained":false,"durationBucket":58,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/3_e-_jmZ8cM","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"3_e-_jmZ8cM","title":"Gen Z Snowflake Has Legendary Woke Meltdown!","duration":58,"uploadDate":"20250508","hasTranscript":true},{"slug":"the-quartering/Dyew9tesYCo","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"Dyew9tesYCo","title":"Gen Z Snowflake Has Legendary Woke Meltdown!","duration":58,"uploadDate":"20240605","hasTranscript":true}]},{"clusterId":"cd1cd7468b179cb05d387b9a5e721adcce6610ff","matchKind":"transcript-near","score":0.896,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/uyTWvbyhtmI","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"uyTWvbyhtmI","title":"Rich Woke Protestors Ruthlessly Roasted On SNL","duration":59,"uploadDate":"20250605","hasTranscript":true},{"slug":"the-quartering/Qj8DbbbQ31Q","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"Qj8DbbbQ31Q","title":"Rich Woke Protestors Ruthlessly Roasted On SNL","duration":59,"uploadDate":"20240509","hasTranscript":true}]},{"clusterId":"f35d1f7c97f94eed2d4a0c4614eae73ba0317179","matchKind":"transcript-near","score":0.895,"contained":false,"durationBucket":60,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/ECqzIXlCIBM","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"ECqzIXlCIBM","title":"Behold Pure Insanity!","duration":60,"uploadDate":"20250517","hasTranscript":true},{"slug":"the-quartering/2yo_GTeT7Kc","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"2yo_GTeT7Kc","title":"Behold Pure Insanity!","duration":60,"uploadDate":"20221012","hasTranscript":true}]},{"clusterId":"6562116177f8474e1abe32737315f0401e8cf2a1","matchKind":"transcript-near","score":0.877,"contained":false,"durationBucket":57,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/AoDIOHo3mOA","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"AoDIOHo3mOA","title":"Unhinged Woke Leftist Has Delusional Meltdown","duration":57,"uploadDate":"20250527","hasTranscript":true},{"slug":"the-quartering/uG7ue-Sw76U","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"uG7ue-Sw76U","title":"Unhinged Woke Leftist Has Delusional Meltdown","duration":57,"uploadDate":"20240516","hasTranscript":true}]},{"clusterId":"4184b978c92af2c0eb6b5705260cd165fe7847f2","matchKind":"transcript-near","score":0.868,"contained":false,"durationBucket":57,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/-0I4Tzqsgqc","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"-0I4Tzqsgqc","title":"She Is PROOF The End Is Near","duration":57,"uploadDate":"20260318","hasTranscript":true},{"slug":"the-quartering/Afzsvjo1a8k","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"Afzsvjo1a8k","title":"She Is PROOF The End Is Near","duration":57,"uploadDate":"20221114","hasTranscript":true}]},{"clusterId":"b87dcb06fc454475b68ddc6617c672a1ae32ccff","matchKind":"transcript-near","score":0.855,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/Ftas2tGbzbU","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"Ftas2tGbzbU","title":"Youtube BANNED This Donald Trump Ad! Why","duration":59,"uploadDate":"20250507","hasTranscript":true},{"slug":"the-quartering/7MC6IBxPTTw","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"7MC6IBxPTTw","title":"Youtube BANNED This Donald Trump Ad! Why?","duration":59,"uploadDate":"20240507","hasTranscript":true}]},{"clusterId":"36249063e82c0991526149be801f66aaeda33aa8","matchKind":"transcript-near","score":0.853,"contained":false,"durationBucket":58,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/IaSnQmlc5t8","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"IaSnQmlc5t8","title":"Proof This Country Is Doomed!","duration":58,"uploadDate":"20250611","hasTranscript":true},{"slug":"the-quartering/0NqnN6BPzjA","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"0NqnN6BPzjA","title":"Proof This Country Is Doomed!","duration":58,"uploadDate":"20221021","hasTranscript":true}]},{"clusterId":"925b8d3ea01528646292498150af5efbdbca24d2","matchKind":"transcript-near","score":0.847,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/u6ezBywKGLY","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"u6ezBywKGLY","title":"Woke MORON Protestor For Palestine Has MELTDOWN Over Basic Questions","duration":59,"uploadDate":"20250606","hasTranscript":true},{"slug":"the-quartering/UMce-R_Zu6Y","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"UMce-R_Zu6Y","title":"Woke MORON Protestor For Palestine Has MELTDOWN Over Basic Questions","duration":59,"uploadDate":"20240502","hasTranscript":true}]},{"clusterId":"a5e2a8f46148eb9a32cf9b787ceed74ca53b3e96","matchKind":"transcript-near","score":0.846,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/MjDefenDv_Q","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"MjDefenDv_Q","title":"Woke Dude RAGES About Pronouns","duration":59,"uploadDate":"20250507","hasTranscript":true},{"slug":"the-quartering/RuVFxMCr8ig","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"RuVFxMCr8ig","title":"Woke Dude RAGES About Pronouns","duration":59,"uploadDate":"20240824","hasTranscript":true}]},{"clusterId":"c365d56360fefb7dea648d280ca5433c7c276c47","matchKind":"transcript-near","score":0.843,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/LBq4uuXMk4M","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"LBq4uuXMk4M","title":"Gen Z Is Disgusting","duration":59,"uploadDate":"20250523","hasTranscript":true},{"slug":"the-quartering/hfmZ-2HXM3w","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"hfmZ-2HXM3w","title":"Gen Z Is Disgusting","duration":59,"uploadDate":"20240807","hasTranscript":true}]},{"clusterId":"62aa65b9a734ba2be0bbaa7cc767f075eb62188c","matchKind":"transcript-near","score":0.84,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/7nLuG3yaD-4","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"7nLuG3yaD-4","title":"Feminist Security System","duration":59,"uploadDate":"20250615","hasTranscript":true},{"slug":"the-quartering/0PkRK_SFi9o","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"0PkRK_SFi9o","title":"Feminist Security System","duration":59,"uploadDate":"20240826","hasTranscript":true}]},{"clusterId":"53ee6f21aab1a1094c798ce92150eda254ce539a","matchKind":"transcript-near","score":0.837,"contained":false,"durationBucket":92,"crossPlatform":true,"crossChannel":true,"videoRefs":[{"slug":"the-quartering-rumble/v73a5us","channelSlug":"the-quartering-rumble","channel":"TheQuartering (Rumble)","platform":"rumble","id":"v73a5us","title":"Women's Biggest Lie","duration":92,"uploadDate":"20260211","hasTranscript":true},{"slug":"the-quartering/kdPEP9ING3A","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"kdPEP9ING3A","title":"Women's Biggest Lie","duration":93,"uploadDate":"20250626","hasTranscript":true}]},{"clusterId":"a4c6fa82a26468408a2addb8f345df60701d3bf3","matchKind":"transcript-near","score":0.837,"contained":false,"durationBucket":60,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/pco8ARw3HkA","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"pco8ARw3HkA","title":"Woke Gen Z Clown Accidently Makes AMAZING Trump Ad","duration":60,"uploadDate":"20250508","hasTranscript":true},{"slug":"the-quartering/WCueE1C1Y7c","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"WCueE1C1Y7c","title":"Woke Gen Z Clown Accidently Makes AMAZING Trump Ad","duration":60,"uploadDate":"20240608","hasTranscript":true}]},{"clusterId":"b1abb0d712aab9abf548d08fe32e38e1c5505016","matchKind":"transcript-near","score":0.835,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/Kq-4qm46d8s","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"Kq-4qm46d8s","title":"Gen Z Woke Brat Has HUMILIATING Obsession!","duration":59,"uploadDate":"20250528","hasTranscript":true},{"slug":"the-quartering/2HdsQCV8F3w","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"2HdsQCV8F3w","title":"Gen Z Woke Brat Has HUMILIATING Obsession!","duration":59,"uploadDate":"20240620","hasTranscript":true}]},{"clusterId":"48746756c170a96f6cfdcfc531b40a40b2ffbab5","matchKind":"transcript-near","score":0.832,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/YPC2Tbe1E24","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"YPC2Tbe1E24","title":"Gen Z Desperation For Attention","duration":59,"uploadDate":"20250506","hasTranscript":true},{"slug":"the-quartering/Uo8JZkv5F-A","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"Uo8JZkv5F-A","title":"Gen Z Desperation For Attention","duration":59,"uploadDate":"20240830","hasTranscript":true}]},{"clusterId":"b9db2262e6485765ed9b4fe6aa0c2d9ed5c1a2d7","matchKind":"transcript-near","score":0.827,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/P7tKZveDw3A","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"P7tKZveDw3A","title":"Woke Unhinged Woman Accidently Tells The Truth","duration":59,"uploadDate":"20250522","hasTranscript":true},{"slug":"the-quartering/Y7VwB_AH2Ig","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"Y7VwB_AH2Ig","title":"Woke Unhinged Woman Accidently Tells The Truth","duration":59,"uploadDate":"20240803","hasTranscript":true}]},{"clusterId":"51e460c577b0b315f08b574d9cab84c37541066c","matchKind":"transcript-near","score":0.823,"contained":false,"durationBucket":58,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/lJsxpaugWIg","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"lJsxpaugWIg","title":"She's Why Men Don't Open Doors","duration":58,"uploadDate":"20250503","hasTranscript":true},{"slug":"the-quartering/Gz6Aghxkf-I","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"Gz6Aghxkf-I","title":"She's Why Men Don't Open Doors","duration":58,"uploadDate":"20230113","hasTranscript":true}]},{"clusterId":"42622fa4b082a82137e13af2a037e71b4517271a","matchKind":"transcript-near","score":0.822,"contained":false,"durationBucket":58,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/ZYaFH3yIzro","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"ZYaFH3yIzro","title":"Gen Z Woman Gets Brutal Reality Check On Biden Inflation","duration":58,"uploadDate":"20250519","hasTranscript":true},{"slug":"the-quartering/piauc7JY94c","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"piauc7JY94c","title":"Gen Z Woman Gets Brutal Reality Check On Biden Inflation","duration":58,"uploadDate":"20240524","hasTranscript":true}]},{"clusterId":"e947040defec16ae21bf1acdcdc9f235c5b241e1","matchKind":"transcript-near","score":0.822,"contained":false,"durationBucket":60,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/zRlskgnxJRs","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"zRlskgnxJRs","title":"Instant Regret For SJW Woman","duration":60,"uploadDate":"20250517","hasTranscript":true},{"slug":"the-quartering/Zk07nJAWx9o","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"Zk07nJAWx9o","title":"Instant Regret For SJW Woman","duration":60,"uploadDate":"20220729","hasTranscript":true}]},{"clusterId":"536b82d8113389beb2f41ec6c4c0a7f0de503ad3","matchKind":"transcript-near","score":0.821,"contained":false,"durationBucket":58,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/xZJfICKdm2I","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"xZJfICKdm2I","title":"She's A Totally INSANE Feminist","duration":58,"uploadDate":"20250526","hasTranscript":true},{"slug":"the-quartering/Z5u5YKmd_-g","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"Z5u5YKmd_-g","title":"She's A Totally INSANE Feminist","duration":58,"uploadDate":"20230118","hasTranscript":true}]},{"clusterId":"7948a9e083dffed5ebbb6097ed06f15114d8fb31","matchKind":"transcript-near","score":0.821,"contained":false,"durationBucket":57,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/UlykmtAIbmI","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"UlykmtAIbmI","title":"Woke Reporter Heartbroken When They Don't Care","duration":58,"uploadDate":"20250617","hasTranscript":true},{"slug":"the-quartering/xgYQ60s20J0","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"xgYQ60s20J0","title":"Woke Reporter Heartbroken When They Don't Care","duration":57,"uploadDate":"20241008","hasTranscript":true}]},{"clusterId":"f1bd52ac6baeb92e1f7c80d969d63b3c486116cd","matchKind":"transcript-near","score":0.818,"contained":false,"durationBucket":58,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/7mcHgXCh3VQ","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"7mcHgXCh3VQ","title":"Based Chic Destroys Modern Dating #dating #woke","duration":58,"uploadDate":"20250618","hasTranscript":true},{"slug":"the-quartering/3a1SPfQzHHg","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"3a1SPfQzHHg","title":"Based Chic Destroys Modern Dating #dating #woke","duration":58,"uploadDate":"20240629","hasTranscript":true}]},{"clusterId":"285b932cdaa3e388557ce1eb4ecfed029aec17c4","matchKind":"transcript-near","score":0.817,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/xTwfefJbjJY","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"xTwfefJbjJY","title":"Woke Gen Z Brat RAGES People KEEP Babies Instead Of Getting Rid Of Them","duration":59,"uploadDate":"20250502","hasTranscript":true},{"slug":"the-quartering/mjqIUBEXoLM","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"mjqIUBEXoLM","title":"Woke Gen Z Brat RAGES People KEEP Babies Instead Of \"Getting Rid\" Of Them","duration":59,"uploadDate":"20240615","hasTranscript":true}]},{"clusterId":"628abd710ec6ef856c26684da970951b2d29cdd8","matchKind":"transcript-near","score":0.812,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/9BELCq-S6_U","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"9BELCq-S6_U","title":"Her Husband Left Her For This!","duration":59,"uploadDate":"20250603","hasTranscript":true},{"slug":"the-quartering/ZGZr3dBvW2U","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"ZGZr3dBvW2U","title":"Her Husband Left Her For This!","duration":59,"uploadDate":"20221130","hasTranscript":true}]},{"clusterId":"1f8ed84159a2c83e538961a2bfd67f2d1081dce4","matchKind":"transcript-near","score":0.811,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/VvbUmwzvXbg","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"VvbUmwzvXbg","title":"Brutal Reality Check For Woke Queers For Palestine","duration":59,"uploadDate":"20250605","hasTranscript":true},{"slug":"the-quartering/0jgDUABX9UY","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"0jgDUABX9UY","title":"Brutal Reality Check For Woke Queers For Palestine","duration":59,"uploadDate":"20240613","hasTranscript":true}]},{"clusterId":"7a9baca688a9d287df454a4915363d6ec89cb44e","matchKind":"transcript-near","score":0.806,"contained":false,"durationBucket":51,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/UCawYLdUw4g","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"UCawYLdUw4g","title":"Worst Girlfriend Ever","duration":51,"uploadDate":"20250525","hasTranscript":true},{"slug":"the-quartering/ApE_ldcvw2E","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"ApE_ldcvw2E","title":"Worst Girlfriend Ever?","duration":51,"uploadDate":"20230111","hasTranscript":true}]},{"clusterId":"821d8e794a9552f8427747ba893be161f689267c","matchKind":"transcript-near","score":0.802,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/CrDfKiD0wmc","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"CrDfKiD0wmc","title":"Gen Z Claims White Only Cities Are Everywhere!","duration":60,"uploadDate":"20250522","hasTranscript":true},{"slug":"the-quartering/TUbulLVrKp0","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"TUbulLVrKp0","title":"Gen Z Claims White Only Cities Are Everywhere!","duration":59,"uploadDate":"20240813","hasTranscript":true}]},{"clusterId":"a3e897aa3de13f85d2ec9b72cc9897d8e2bbf6a1","matchKind":"transcript-near","score":0.802,"contained":false,"durationBucket":60,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/qUWN8Ca9de4","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"qUWN8Ca9de4","title":"Woke Chic Demands Sperm Donor Pay Child Support","duration":60,"uploadDate":"20250515","hasTranscript":true},{"slug":"the-quartering/V7QCdik1opg","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"V7QCdik1opg","title":"Woke Chic Demands Sperm Donor Pay Child Support","duration":60,"uploadDate":"20240601","hasTranscript":true}]},{"clusterId":"985594f57486a1143dc6048144649b2388838fb1","matchKind":"transcript-near","score":0.8,"contained":false,"durationBucket":60,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/ndzdxfFD74s","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"ndzdxfFD74s","title":"Gen Z Brats Immediately Fired","duration":60,"uploadDate":"20250502","hasTranscript":true},{"slug":"the-quartering/wKAfh3U-tr0","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"wKAfh3U-tr0","title":"Gen Z Brats Immediately Fired","duration":60,"uploadDate":"20240523","hasTranscript":true}]},{"clusterId":"f419d6988cd680388a1ebbc7fdcebeda5488f841","matchKind":"transcript-near","score":0.8,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/8GSWo2-tnRo","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"8GSWo2-tnRo","title":"Proof Women Are The Worst","duration":59,"uploadDate":"20250523","hasTranscript":true},{"slug":"the-quartering/Ny9ZC5Wrhew","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"Ny9ZC5Wrhew","title":"Proof Women Are The Worst","duration":59,"uploadDate":"20240901","hasTranscript":true}]},{"clusterId":"c793dae85d3a9a818443806b62214f16f65b1aff","matchKind":"transcript-near","score":0.789,"contained":false,"durationBucket":58,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/hO2DEy24ubs","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"hO2DEy24ubs","title":"Woke Gen Z Chic Destroyed!","duration":58,"uploadDate":"20250516","hasTranscript":true},{"slug":"the-quartering/ergDunXsZNA","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"ergDunXsZNA","title":"Woke Gen Z Chic Destroyed!","duration":58,"uploadDate":"20241016","hasTranscript":true}]},{"clusterId":"4d10378cedaf021cc7eace0cf838479c4a349515","matchKind":"transcript-near","score":0.788,"contained":false,"durationBucket":58,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/xEpJgYG1d_g","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"xEpJgYG1d_g","title":"Fire This Teacher Now!","duration":58,"uploadDate":"20250601","hasTranscript":true},{"slug":"the-quartering/8Ki1DN4c8M0","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"8Ki1DN4c8M0","title":"Fire This Teacher Now!","duration":58,"uploadDate":"20220823","hasTranscript":true}]},{"clusterId":"899ef58fc026d5bce972f3c27cedf74b284288f4","matchKind":"transcript-near","score":0.785,"contained":false,"durationBucket":60,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/U1sLSWV7Lpo","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"U1sLSWV7Lpo","title":"Woke Feminists DESTROYED By New Question","duration":60,"uploadDate":"20250503","hasTranscript":true},{"slug":"the-quartering/PJQG6QNN_v4","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"PJQG6QNN_v4","title":"Woke Feminists DESTROYED By New Question","duration":60,"uploadDate":"20240531","hasTranscript":true}]},{"clusterId":"8e316ba5045c4192bd97adfcbcd8a9a89d69c7fc","matchKind":"transcript-near","score":0.785,"contained":false,"durationBucket":57,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/3SDj7J0a1KY","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"3SDj7J0a1KY","title":"Is She Psycho Or Perfect","duration":57,"uploadDate":"20250601","hasTranscript":true},{"slug":"the-quartering/1ddVirZdqPY","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"1ddVirZdqPY","title":"Is She Psycho Or Perfect?","duration":57,"uploadDate":"20241003","hasTranscript":true}]},{"clusterId":"f67725ee2741d2f2907f7c9fe96768716345a4b9","matchKind":"transcript-near","score":0.782,"contained":false,"durationBucket":52,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/DIzhwWjfe7M","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"DIzhwWjfe7M","title":"A Flag For Groomers","duration":52,"uploadDate":"20250531","hasTranscript":true},{"slug":"the-quartering/2_j-4ex0BMg","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"2_j-4ex0BMg","title":"A Flag For Groomers?","duration":52,"uploadDate":"20230130","hasTranscript":true}]},{"clusterId":"4753782ac8bd65d396db3129972c56f851a886fc","matchKind":"transcript-near","score":0.779,"contained":false,"durationBucket":57,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/ruMDf1Weq48","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"ruMDf1Weq48","title":"Woke Clown Can't Get A Job! I Wonder Why","duration":57,"uploadDate":"20250503","hasTranscript":true},{"slug":"the-quartering/pDhxbos16sk","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"pDhxbos16sk","title":"Woke Clown Can't Get A Job! I Wonder Why?","duration":57,"uploadDate":"20240528","hasTranscript":true}]},{"clusterId":"f613fa73a06912c296d7862e3bd659a85de8bfc8","matchKind":"transcript-near","score":0.776,"contained":false,"durationBucket":57,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/Wp7f2h0NhEc","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"Wp7f2h0NhEc","title":"Woke Gen Z Demolished By Based Chic","duration":57,"uploadDate":"20250517","hasTranscript":true},{"slug":"the-quartering/m-U1MFUdio8","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"m-U1MFUdio8","title":"Woke Gen Z Demolished By Based Chic","duration":57,"uploadDate":"20240909","hasTranscript":true}]},{"clusterId":"75b53bda197934da5bfffbc4aaf71ee343c4fbf1","matchKind":"transcript-near","score":0.768,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/FCh-NN9iQIg","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"FCh-NN9iQIg","title":"Creepy Teacher Finally ADMITS Her Plan","duration":59,"uploadDate":"20250506","hasTranscript":true},{"slug":"the-quartering/YHK6ojkn07I","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"YHK6ojkn07I","title":"Creepy Teacher Finally ADMITS Her Plan","duration":59,"uploadDate":"20221115","hasTranscript":true}]},{"clusterId":"848c14f89800e62a2eacf3e79c7c9a7e582490c3","matchKind":"transcript-near","score":0.762,"contained":false,"durationBucket":57,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/Sh0PgkVxiUo","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"Sh0PgkVxiUo","title":"If You See This Call The Cops!!!","duration":57,"uploadDate":"20250504","hasTranscript":true},{"slug":"the-quartering/v9qAzVOG0KU","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"v9qAzVOG0KU","title":"If You See This..Call The Cops!!!","duration":57,"uploadDate":"20221230","hasTranscript":true}]},{"clusterId":"bfc4dd902505a4803b1d68025417aa5e6ffcb0fa","matchKind":"transcript-near","score":0.753,"contained":false,"durationBucket":57,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/_GiB6I4-67E","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"_GiB6I4-67E","title":"Teacher DESTROYED By Based Student!","duration":57,"uploadDate":"20250509","hasTranscript":true},{"slug":"the-quartering/egc_OSxBW-A","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"egc_OSxBW-A","title":"Teacher DESTROYED By Based Student!","duration":57,"uploadDate":"20230306","hasTranscript":true}]},{"clusterId":"ba345c1d5d9bc4b57b2e63ef253091ca9feff9d2","matchKind":"transcript-near","score":0.749,"contained":false,"durationBucket":157,"crossPlatform":true,"crossChannel":true,"videoRefs":[{"slug":"the-quartering-rumble/v769wg4","channelSlug":"the-quartering-rumble","channel":"TheQuartering (Rumble)","platform":"rumble","id":"v769wg4","title":"This Teacher Absolutely Loses The Plot #comedy #parody #viral #funny","duration":157,"uploadDate":"20260413","hasTranscript":true},{"slug":"the-quartering/jy6EU6qPHxc","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"jy6EU6qPHxc","title":"This Teacher Absolutely Loses The Plot #comedy #parody #viral #funny","duration":157,"uploadDate":"20260413","hasTranscript":true}]},{"clusterId":"2919bf99d76b7f7f50314f2690246168bd5b3884","matchKind":"transcript-near","score":0.746,"contained":false,"durationBucket":57,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/XHxosSVbjzI","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"XHxosSVbjzI","title":"Her WOKE Parents RUINED Her!","duration":57,"uploadDate":"20250528","hasTranscript":true},{"slug":"the-quartering/6khHTBFH9Tk","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"6khHTBFH9Tk","title":"Her WOKE Parents RUINED Her!","duration":57,"uploadDate":"20230203","hasTranscript":true}]},{"clusterId":"d6839e77f78bd2b930bf29f8060516d68ab2bc30","matchKind":"transcript-near","score":0.743,"contained":false,"durationBucket":60,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/xie9OpqmKBk","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"xie9OpqmKBk","title":"Sylvester Stallone DESTROYS Woke Gen Z","duration":60,"uploadDate":"20250519","hasTranscript":true},{"slug":"the-quartering/lea0arKA0gg","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"lea0arKA0gg","title":"Sylvester Stallone DESTROYS Woke Gen Z","duration":60,"uploadDate":"20240511","hasTranscript":true}]},{"clusterId":"765a26bedd752faac6deb532a8d659b7cf946420","matchKind":"transcript-near","score":0.74,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/MBaVpQO79FI","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"MBaVpQO79FI","title":"Woke Gen Z Dude MELTDOWN Over Pronouns!","duration":59,"uploadDate":"20250526","hasTranscript":true},{"slug":"the-quartering/g9ziJKsEyfg","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"g9ziJKsEyfg","title":"Woke Gen Z Dude MELTDOWN Over Pronouns!","duration":59,"uploadDate":"20250121","hasTranscript":true}]},{"clusterId":"6371c6658fe6bcdac32f3ca45e5f11cab34d439a","matchKind":"transcript-near","score":0.739,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/kneUaTzwf7w","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"kneUaTzwf7w","title":"She Destroys Man Hating Chic","duration":59,"uploadDate":"20250526","hasTranscript":true},{"slug":"the-quartering/eslKs15T03k","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"eslKs15T03k","title":"She Destroys Man Hating Chic","duration":59,"uploadDate":"20230130","hasTranscript":true}]},{"clusterId":"9517723da5b4b2478d8ccb3156cb18e066541965","matchKind":"transcript-near","score":0.736,"contained":false,"durationBucket":60,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/tF2YRSIikdw","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"tF2YRSIikdw","title":"Woke Staff Immediately Fired","duration":60,"uploadDate":"20250504","hasTranscript":true},{"slug":"the-quartering/-Ku-GxLKh3g","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"-Ku-GxLKh3g","title":"Woke Staff Immediately Fired","duration":60,"uploadDate":"20240620","hasTranscript":true}]},{"clusterId":"043dbf5926317d69ced0681c4213f5aa5e798a1d","matchKind":"transcript-near","score":0.734,"contained":false,"durationBucket":56,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/9W_NZB8sw3I","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"9W_NZB8sw3I","title":"Toddler Drops Red Pill On Live TV","duration":56,"uploadDate":"20250512","hasTranscript":true},{"slug":"the-quartering/1QCI6-HzGiM","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"1QCI6-HzGiM","title":"Toddler Drops Red Pill On Live TV","duration":56,"uploadDate":"20240521","hasTranscript":true}]},{"clusterId":"e1c99258be13444353170efbff2ca5b0235bba26","matchKind":"transcript-near","score":0.731,"contained":false,"durationBucket":58,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/dcB8mRMKW2U","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"dcB8mRMKW2U","title":"Entitled Gen Z Protester Brain SHUTS DOWN When EXPOSED!","duration":58,"uploadDate":"20250604","hasTranscript":true},{"slug":"the-quartering/Dc5gq8g1XwA","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"Dc5gq8g1XwA","title":"Entitled Gen Z Protester Brain SHUTS DOWN When EXPOSED!","duration":58,"uploadDate":"20240608","hasTranscript":true}]},{"clusterId":"80eb0eb3d84359046ee368140369c654ffdae287","matchKind":"transcript-near","score":0.728,"contained":false,"durationBucket":58,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/_MTfGpHYlBM","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"_MTfGpHYlBM","title":"Woke Teacher Destroyed","duration":58,"uploadDate":"20250606","hasTranscript":true},{"slug":"the-quartering/ILHYUNpn50I","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"ILHYUNpn50I","title":"Woke Teacher Destroyed","duration":58,"uploadDate":"20241002","hasTranscript":true}]},{"clusterId":"966a78e92a204ae166da64d1bed7c58e29b023b2","matchKind":"transcript-near","score":0.724,"contained":false,"durationBucket":60,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/EuKDwWdgCug","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"EuKDwWdgCug","title":"Cute Girl Destroys Woke Left","duration":60,"uploadDate":"20250529","hasTranscript":true},{"slug":"the-quartering/nl-QjLdaqoQ","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"nl-QjLdaqoQ","title":"Cute Girl Destroys Woke Left","duration":60,"uploadDate":"20240905","hasTranscript":true}]},{"clusterId":"ef1b25b6c4f283090eed5df69ed45fb6de886d5a","matchKind":"transcript-near","score":0.719,"contained":false,"durationBucket":57,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/PQN1jln0DNA","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"PQN1jln0DNA","title":"The DUMBEST Woman On The Planet","duration":57,"uploadDate":"20250515","hasTranscript":true},{"slug":"the-quartering/56PIpqfpyBw","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"56PIpqfpyBw","title":"The DUMBEST Woman On The Planet","duration":57,"uploadDate":"20240524","hasTranscript":true}]},{"clusterId":"742bedce66217752d61b3f703ceea9462e95a501","matchKind":"transcript-near","score":0.713,"contained":false,"durationBucket":60,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/xmfycniqy1I","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"xmfycniqy1I","title":"Woke Healthcare Is Mind Blowing","duration":60,"uploadDate":"20250504","hasTranscript":true},{"slug":"the-quartering/2tk_nMTQXGs","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"2tk_nMTQXGs","title":"Woke Healthcare Is Mind Blowing","duration":60,"uploadDate":"20240526","hasTranscript":true}]},{"clusterId":"5af34cf19f3eac15112c4be66f901b6ab99bd5cb","matchKind":"transcript-near","score":0.712,"contained":false,"durationBucket":60,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/ofOYVH_kJhw","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"ofOYVH_kJhw","title":"Donald Trump Vindicated!","duration":60,"uploadDate":"20250505","hasTranscript":true},{"slug":"the-quartering/9HOkAS2jIr8","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"9HOkAS2jIr8","title":"Donald Trump Vindicated!","duration":60,"uploadDate":"20240514","hasTranscript":true}]},{"clusterId":"a0e89c3e84703cdc207879ee4901fc3e29ee510a","matchKind":"transcript-near","score":0.707,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/_TyuEK6oDio","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"_TyuEK6oDio","title":"Woke Gen Z Dude Humiliated","duration":59,"uploadDate":"20250513","hasTranscript":true},{"slug":"the-quartering/6oDd_bTnSxc","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"6oDd_bTnSxc","title":"Woke Gen Z Dude Humiliated","duration":59,"uploadDate":"20241003","hasTranscript":true}]},{"clusterId":"ddc2ca8c9bdc8d9da64b023e59556e42856b6f7f","matchKind":"transcript-near","score":0.7,"contained":false,"durationBucket":60,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/o1CUgKb0djA","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"o1CUgKb0djA","title":"Feminist DESTROYED By Pretty Girl!","duration":60,"uploadDate":"20250512","hasTranscript":true},{"slug":"the-quartering/Eq7nyMM2seA","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"Eq7nyMM2seA","title":"Feminist DESTROYED By Pretty Girl!","duration":60,"uploadDate":"20221102","hasTranscript":true}]},{"clusterId":"f258f3f16bd07712aa54315adc9e6912fd07472d","matchKind":"transcript-near","score":0.7,"contained":false,"durationBucket":60,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/1_NAus1i5GY","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"1_NAus1i5GY","title":"Doctor DESTROYS Woke Propaganda","duration":60,"uploadDate":"20250518","hasTranscript":true},{"slug":"the-quartering/GacDrqzmqQU","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"GacDrqzmqQU","title":"Doctor DESTROYS Woke Propaganda","duration":60,"uploadDate":"20221024","hasTranscript":true}]},{"clusterId":"fb3f163f3f25a43f8ee301395b4830f312a95dae","matchKind":"transcript-near","score":0.694,"contained":false,"durationBucket":57,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/bNnSAQLMPO0","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"bNnSAQLMPO0","title":"Woke Gen Z Destroyed! Sports Edition","duration":57,"uploadDate":"20250609","hasTranscript":true},{"slug":"the-quartering/1kvMC38wNoU","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"1kvMC38wNoU","title":"Woke Gen Z Destroyed! Sports Edition","duration":57,"uploadDate":"20241027","hasTranscript":true}]},{"clusterId":"5f1515ba4ea15ddba017f3227acef3c4662c8780","matchKind":"transcript-near","score":0.692,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/GvOi44Z-9kg","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"GvOi44Z-9kg","title":"Woke Clown Destroyed By 100 Year Old","duration":59,"uploadDate":"20250602","hasTranscript":true},{"slug":"the-quartering/3ZYvcaZNnyc","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"3ZYvcaZNnyc","title":"98 Year Old Man Destroys FAKE Woke Palestine College Demonstrations","duration":59,"uploadDate":"20240502","hasTranscript":true}]},{"clusterId":"5f797c2d8861e002209472ac428b53548b4b5b84","matchKind":"transcript-near","score":0.684,"contained":false,"durationBucket":60,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/1Nwpml6vEwQ","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"1Nwpml6vEwQ","title":"Proof This Woke Stuff Is Fake","duration":60,"uploadDate":"20250614","hasTranscript":true},{"slug":"the-quartering/heHpNNfjAvc","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"heHpNNfjAvc","title":"Proof This Woke Stuff Is Fake","duration":60,"uploadDate":"20220901","hasTranscript":true}]},{"clusterId":"551904a85235b2104b29b5f3bc320dfc8a4ea205","matchKind":"transcript-near","score":0.681,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/nCu3VcjUIQk","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"nCu3VcjUIQk","title":"Bud Light REMOVED From Costco! Layoffs Coming! #budlight #news","duration":59,"uploadDate":"20250601","hasTranscript":true},{"slug":"the-quartering/NPNoNXwlyss","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"NPNoNXwlyss","title":"Bud Light REMOVED From Costco! Layoffs Coming! #budlight #news","duration":59,"uploadDate":"20230713","hasTranscript":true}]},{"clusterId":"2bb31d6c861142f7ccf6047664592663d4afbcc4","matchKind":"transcript-near","score":0.68,"contained":false,"durationBucket":121,"crossPlatform":true,"crossChannel":true,"videoRefs":[{"slug":"the-quartering-rumble/v73a4xu","channelSlug":"the-quartering-rumble","channel":"TheQuartering (Rumble)","platform":"rumble","id":"v73a4xu","title":"Alligator Alcatraz Meltdown","duration":121,"uploadDate":"20260210","hasTranscript":true},{"slug":"the-quartering/38j-M3SdtM8","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"38j-M3SdtM8","title":"Alligator Alcatraz Meltdown","duration":122,"uploadDate":"20250715","hasTranscript":true}]},{"clusterId":"85d83ea9468680bd2f30a1ec09dc2f774e28612d","matchKind":"transcript-near","score":0.678,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/F59Xm3i9xbk","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"F59Xm3i9xbk","title":"Garbage Mom Gets Owned","duration":59,"uploadDate":"20250530","hasTranscript":true},{"slug":"the-quartering/P-qiPb5Kykk","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"P-qiPb5Kykk","title":"Garbage Mom Gets Owned","duration":59,"uploadDate":"20250110","hasTranscript":true}]},{"clusterId":"182b979b9d2d35062636bcfd05c89674ef952314","matchKind":"transcript-near","score":0.677,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/QX7ZHTpZIes","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"QX7ZHTpZIes","title":"Woke Creep FREAKOUT Caught On Camera","duration":59,"uploadDate":"20250514","hasTranscript":true},{"slug":"the-quartering/avPK1q3S5H4","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"avPK1q3S5H4","title":"Woke Creep FREAKOUT Caught On Camera","duration":59,"uploadDate":"20240810","hasTranscript":true}]},{"clusterId":"032e28872c4d0436b0d9b0c520ef5ece1e40cacf","matchKind":"transcript-near","score":0.665,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/498UfZJF1oA","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"498UfZJF1oA","title":"Woke Gen Z Mom Kids Destroyed By Home School","duration":59,"uploadDate":"20250509","hasTranscript":true},{"slug":"the-quartering/FXvP1GNlRHc","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"FXvP1GNlRHc","title":"Woke Gen Z Mom Kids Destroyed By Home School","duration":59,"uploadDate":"20240623","hasTranscript":true}]},{"clusterId":"d93d1145c1e208ba0b1960d0f312e1fbb6b9b1ce","matchKind":"transcript-near","score":0.665,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/TP6lt86mDhw","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"TP6lt86mDhw","title":"Woke Waitress Meltdown!","duration":59,"uploadDate":"20250602","hasTranscript":true},{"slug":"the-quartering/QL2i1A6BuIg","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"QL2i1A6BuIg","title":"Woke Waitress Meltdown!","duration":59,"uploadDate":"20241011","hasTranscript":true}]},{"clusterId":"9d7f5c9c9e9a60c9c240b61294eec903073e7ae3","matchKind":"transcript-near","score":0.651,"contained":false,"durationBucket":94,"crossPlatform":true,"crossChannel":true,"videoRefs":[{"slug":"the-quartering-rumble/v73a4wc","channelSlug":"the-quartering-rumble","channel":"TheQuartering (Rumble)","platform":"rumble","id":"v73a4wc","title":"Epstein Lies Are INSANE","duration":94,"uploadDate":"20260210","hasTranscript":true},{"slug":"the-quartering/cp09l7E-ZT0","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"cp09l7E-ZT0","title":"Epstein Lies Are INSANE","duration":94,"uploadDate":"20250717","hasTranscript":true}]},{"clusterId":"6352ece7b403069cb8f0f43ca11bc64a8bf32444","matchKind":"transcript-near","score":0.65,"contained":false,"durationBucket":58,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/M1V0Q7f3iP0","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"M1V0Q7f3iP0","title":"Feminist Destroyed By Another Woman","duration":58,"uploadDate":"20250507","hasTranscript":true},{"slug":"the-quartering/OCUVUDg2akE","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"OCUVUDg2akE","title":"Feminist Destroyed By Another Woman","duration":58,"uploadDate":"20230124","hasTranscript":true}]},{"clusterId":"906c515874c5500bc1816eb02950af6e58370aa0","matchKind":"transcript-near","score":0.65,"contained":false,"durationBucket":50,"crossPlatform":false,"crossChannel":false,"videoRefs":[{"slug":"the-quartering/4OnUZogT8tI","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"4OnUZogT8tI","title":"She Is RUINING Her Kids","duration":50,"uploadDate":"20221229","hasTranscript":true},{"slug":"the-quartering/rm9cUkg8otY","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"rm9cUkg8otY","title":"Please STOP Having Kids Now!","duration":50,"uploadDate":"20230301","hasTranscript":true}]},{"clusterId":"cbe474cef8f41c6e9988fefa371574c0896669ef","matchKind":"transcript-near","score":0.645,"contained":false,"durationBucket":39,"crossPlatform":false,"crossChannel":false,"videoRefs":[{"slug":"chrissie-mayr/Sex9Wgt5hO4","channelSlug":"chrissie-mayr","channel":"Chrissie Mayr","platform":"youtube","id":"Sex9Wgt5hO4","title":"Chrissie Mayr Shot Hesitancy! Stand Up Comedy from Mic Drop Comedy Club in San Diego, California","duration":39,"uploadDate":"20230302","hasTranscript":true},{"slug":"chrissie-mayr/SY1Dnm7gchs","channelSlug":"chrissie-mayr","channel":"Chrissie Mayr","platform":"youtube","id":"SY1Dnm7gchs","title":"Chrissie Mayr on Vaccine Push! Stand Up Comedy Clip! Mic Drop Comedy Club in San Diego, California","duration":39,"uploadDate":"20230228","hasTranscript":true}]},{"clusterId":"b415d6a3c0e295eccd9067065b337838685a1bdb","matchKind":"transcript-near","score":0.644,"contained":false,"durationBucket":60,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/-P3f1Bs1nS0","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"-P3f1Bs1nS0","title":"Illegal Taunts Trump Immediately Regrets It","duration":60,"uploadDate":"20250529","hasTranscript":true},{"slug":"the-quartering/fefnvrlpqDY","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"fefnvrlpqDY","title":"Illegal Taunts Trump Immediately Regrets It","duration":60,"uploadDate":"20250202","hasTranscript":true}]},{"clusterId":"b931ce1d0e7c369f73b96d1314857b7f7005a253","matchKind":"transcript-near","score":0.635,"contained":false,"durationBucket":58,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/I2-WNH9-WwE","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"I2-WNH9-WwE","title":"Woke Reporter Humiliated & Runs!","duration":58,"uploadDate":"20250530","hasTranscript":true},{"slug":"the-quartering/7_sbqVuTPXQ","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"7_sbqVuTPXQ","title":"Woke Reporter Humiliated & Runs!","duration":58,"uploadDate":"20241004","hasTranscript":true}]},{"clusterId":"a65498ee64ef15cfbc6d0a8076be1910903ab057","matchKind":"transcript-near","score":0.633,"contained":false,"durationBucket":133,"crossPlatform":true,"crossChannel":true,"videoRefs":[{"slug":"the-quartering-rumble/v1by0kw","channelSlug":"the-quartering-rumble","channel":"TheQuartering (Rumble)","platform":"rumble","id":"v1by0kw","title":"Today's Big Winners!","duration":133,"uploadDate":"20220802","hasTranscript":true},{"slug":"the-quartering/hM1xy1LMQXM","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"hM1xy1LMQXM","title":"Today's Big Winners!","duration":134,"uploadDate":"20220802","hasTranscript":true}]},{"clusterId":"ca81a746e013241d6c85720ea86233f093658cf8","matchKind":"transcript-near","score":0.628,"contained":false,"durationBucket":57,"crossPlatform":false,"crossChannel":false,"videoRefs":[{"slug":"the-quartering/PZVvYzgWdt8","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"PZVvYzgWdt8","title":"Try Not Cringing Challenge (Impossible)","duration":59,"uploadDate":"20220831","hasTranscript":true},{"slug":"the-quartering/WdJFU0_V4XE","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"WdJFU0_V4XE","title":"Witness Pure Insanity!","duration":57,"uploadDate":"20221125","hasTranscript":true}]},{"clusterId":"b1c0f5740cfb335fc60d308b1d9739ac58cfe90d","matchKind":"transcript-near","score":0.623,"contained":false,"durationBucket":58,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/L0sWH7x5GrA","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"L0sWH7x5GrA","title":"Unhinged Woke TikTok Meltdown!","duration":58,"uploadDate":"20250515","hasTranscript":true},{"slug":"the-quartering/kvcQlBM3HQ8","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"kvcQlBM3HQ8","title":"Unhinged Woke TikTok Meltdown!","duration":58,"uploadDate":"20230201","hasTranscript":true}]},{"clusterId":"9bb6ea7868ca76bf6e15582d129dd99dfb3c5667","matchKind":"transcript-near","score":0.618,"contained":false,"durationBucket":59,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/5x_mn5O6Lew","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"5x_mn5O6Lew","title":"Woke Gen Z Liberal Humiliated","duration":59,"uploadDate":"20250523","hasTranscript":true},{"slug":"the-quartering/yw-qH0FDxpc","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"yw-qH0FDxpc","title":"Woke Gen Z Liberal Humiliated","duration":59,"uploadDate":"20241005","hasTranscript":true}]},{"clusterId":"68e086afbc6a4d2bbb5774bcd2513509b4921906","matchKind":"transcript-near","score":0.612,"contained":false,"durationBucket":143,"crossPlatform":true,"crossChannel":true,"videoRefs":[{"slug":"the-quartering-rumble/vmrnop","channelSlug":"the-quartering-rumble","channel":"TheQuartering (Rumble)","platform":"rumble","id":"vmrnop","title":"I Want You To Come Visit Me...","duration":143,"uploadDate":"20210827","hasTranscript":true},{"slug":"the-quartering/B9_oXLpoL64","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"B9_oXLpoL64","title":"I Want You To Come Visit Me...","duration":144,"uploadDate":"20210827","hasTranscript":true}]},{"clusterId":"447f171b0fa94bc532851a1b8342a6383e3787de","matchKind":"transcript-near","score":0.607,"contained":false,"durationBucket":58,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/XGXHg-M68qA","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"XGXHg-M68qA","title":"Woke Reporter MINDBLOWN By Absolute Chad","duration":58,"uploadDate":"20250602","hasTranscript":true},{"slug":"the-quartering/7I4jre67kCE","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"7I4jre67kCE","title":"Woke Reporter MINDBLOWN By Absolute Chad","duration":58,"uploadDate":"20240902","hasTranscript":true}]},{"clusterId":"3b806a4eec8da290119e776ff2d18b240486d616","matchKind":"transcript-near","score":0.603,"contained":false,"durationBucket":57,"crossPlatform":false,"crossChannel":true,"videoRefs":[{"slug":"jeremy-hambly/eioCXImTrQ8","channelSlug":"jeremy-hambly","channel":"Jeremy Hambly","platform":"youtube","id":"eioCXImTrQ8","title":"Woke Teacher MELTDOWN She Can't Creep Anymore!","duration":57,"uploadDate":"20250610","hasTranscript":true},{"slug":"the-quartering/m3Kyj5KPZKc","channelSlug":"the-quartering","channel":"TheQuartering","platform":"youtube","id":"m3Kyj5KPZKc","title":"Woke Teacher MELTDOWN She Can't Creep Anymore!","duration":57,"uploadDate":"20221107","hasTranscript":true}]}]} -\ No newline at end of file +{"version":1,"generatedAt":"2026-06-05T02:48:49.807Z","runConfig":{"thresholdSeconds":180,"durationToleranceSeconds":2,"nearThreshold":0.6,"containmentThreshold":0.8,"shingleSize":5},"totals":{"videosScanned":34915,"clusters":0,"videosInClusters":0},"clusters":[]} +\ No newline at end of file