const MAX_BLOB_BYTES = 5e6;
const MAX_SCAN_BYTES = 1e7;
function isYoutubeOrGooglevideoHost(hostname) {
  const h = hostname.toLowerCase();
  if (h === "youtube.com" || h === "www.youtube.com") return true;
  if (h.endsWith(".youtube.com")) return true;
  if (h === "googlevideo.com" || h.endsWith(".googlevideo.com")) return true;
  return false;
}
function isSafeCaptionUrl(raw) {
  if (!raw) return false;
  try {
    const u = new URL(raw);
    if (u.protocol !== "https:") return false;
    return isYoutubeOrGooglevideoHost(u.hostname);
  } catch {
    return false;
  }
}
const WATCH_HOSTS = /* @__PURE__ */ new Set([
  "youtube.com",
  "www.youtube.com",
  "m.youtube.com",
  "music.youtube.com"
]);
function isYoutubeHost(hostname) {
  if (WATCH_HOSTS.has(hostname)) return true;
  if (hostname === "youtu.be") return true;
  return false;
}
function isWatchUrl(url) {
  const host = url.hostname;
  if (host === "youtu.be") {
    return url.pathname.length > 1;
  }
  if (WATCH_HOSTS.has(host)) {
    return url.pathname === "/watch" && url.searchParams.has("v");
  }
  return false;
}
function extractJsonBlob(html, marker) {
  const idx = html.indexOf(marker);
  if (idx === -1) return null;
  const braceStart = html.indexOf("{", idx);
  if (braceStart === -1) return null;
  const scanEnd = Math.min(html.length, braceStart + MAX_SCAN_BYTES);
  let depth = 0;
  let inString = false;
  let escaped = false;
  let end = -1;
  for (let i = braceStart; i < scanEnd; i++) {
    const ch = html[i];
    if (escaped) {
      escaped = false;
      continue;
    }
    if (ch === "\\") {
      escaped = true;
      continue;
    }
    if (ch === '"') {
      inString = !inString;
      continue;
    }
    if (inString) continue;
    if (ch === "{") depth++;
    else if (ch === "}") {
      depth--;
      if (depth === 0) {
        end = i;
        break;
      }
    }
  }
  if (end === -1) return null;
  if (end - braceStart > MAX_BLOB_BYTES) return null;
  try {
    return JSON.parse(html.slice(braceStart, end + 1));
  } catch {
    return null;
  }
}
function toNumber(value) {
  if (typeof value === "number" && Number.isFinite(value)) return value;
  if (typeof value === "string") {
    const n = Number(value);
    return Number.isFinite(n) ? n : 0;
  }
  return 0;
}
function toString(value) {
  return typeof value === "string" ? value : "";
}
function formatIsoDuration(totalSeconds) {
  if (!Number.isFinite(totalSeconds) || totalSeconds <= 0) return "PT0S";
  const hours = Math.floor(totalSeconds / 3600);
  const minutes = Math.floor(totalSeconds % 3600 / 60);
  const seconds = totalSeconds % 60;
  let out = "PT";
  if (hours > 0) out += `${hours}H`;
  if (minutes > 0) out += `${minutes}M`;
  if (seconds > 0 || hours === 0 && minutes === 0) out += `${seconds}S`;
  return out;
}
function parseCaptionTracks(player) {
  const captions = player.captions;
  if (!captions) return [];
  const tracklist = captions.playerCaptionsTracklistRenderer;
  if (!tracklist) return [];
  const tracks = tracklist.captionTracks;
  if (!Array.isArray(tracks)) return [];
  const out = [];
  for (const raw of tracks) {
    if (!raw || typeof raw !== "object") continue;
    const t = raw;
    const baseUrl = toString(t.baseUrl);
    if (!baseUrl) continue;
    if (!isSafeCaptionUrl(baseUrl)) continue;
    const nameObj = t.name;
    const name = nameObj ? toString(nameObj.simpleText) : "";
    out.push({
      language_code: toString(t.languageCode),
      base_url: baseUrl,
      kind: toString(t.kind),
      name
    });
  }
  return out;
}
function parseChapters(initialData) {
  if (!initialData || typeof initialData !== "object") return [];
  const playerOverlays = initialData.playerOverlays;
  if (!playerOverlays || typeof playerOverlays !== "object") return [];
  const por = playerOverlays.playerOverlayRenderer;
  if (!por || typeof por !== "object") return [];
  const outer = por.decoratedPlayerBarRenderer;
  if (!outer || typeof outer !== "object") return [];
  const inner = outer.decoratedPlayerBarRenderer;
  if (!inner || typeof inner !== "object") return [];
  const playerBar = inner.playerBar;
  if (!playerBar || typeof playerBar !== "object") return [];
  const multi = playerBar.multiMarkersPlayerBarRenderer;
  if (!multi || typeof multi !== "object") return [];
  const markersMap = multi.markersMap;
  if (!Array.isArray(markersMap)) return [];
  for (const entry of markersMap) {
    if (!entry || typeof entry !== "object") continue;
    const e = entry;
    if (e.key !== "DESCRIPTION_CHAPTERS") continue;
    const value = e.value;
    if (!value) continue;
    const rawChapters = value.chapters;
    if (!Array.isArray(rawChapters)) continue;
    const chapters = [];
    for (const raw of rawChapters) {
      if (!raw || typeof raw !== "object") continue;
      const cr = raw.chapterRenderer;
      if (!cr) continue;
      const titleObj = cr.title;
      const title = titleObj ? toString(titleObj.simpleText) : "";
      const millis = toNumber(cr.timeRangeStartMillis);
      chapters.push({ start: Math.floor(millis / 1e3), title });
    }
    return chapters;
  }
  return [];
}
function buildMarkdown(meta, title, description) {
  const lines = [];
  lines.push(`# ${title}`);
  lines.push("");
  if (meta.channel) lines.push(`**Channel:** ${meta.channel}`);
  if (meta.duration_seconds > 0) {
    lines.push(`**Duration:** ${meta.duration} (${meta.duration_seconds}s)`);
  }
  if (meta.view_count > 0) lines.push(`**Views:** ${meta.view_count.toLocaleString("en-US")}`);
  if (meta.posted_at) lines.push(`**Posted:** ${meta.posted_at}`);
  if (meta.video_id) lines.push(`**Video ID:** ${meta.video_id}`);
  lines.push("");
  if (description) {
    lines.push("## Description");
    lines.push("");
    lines.push(description);
    lines.push("");
  }
  if (meta.chapters.length > 0) {
    lines.push("## Chapters");
    lines.push("");
    for (const ch of meta.chapters) {
      lines.push(`- ${formatTimestamp(ch.start)} \u2014 ${ch.title}`);
    }
    lines.push("");
  }
  if (meta.caption_tracks.length > 0) {
    lines.push("## Captions");
    lines.push("");
    const langs = meta.caption_tracks.map((t) => t.name || t.language_code).join(", ");
    lines.push(`Available caption tracks: ${langs}`);
    lines.push("");
  }
  return lines.join("\n").trim();
}
function formatTimestamp(seconds) {
  if (!Number.isFinite(seconds) || seconds < 0) return "00:00";
  const h = Math.floor(seconds / 3600);
  const m = Math.floor(seconds % 3600 / 60);
  const s = seconds % 60;
  const pad = (n) => n.toString().padStart(2, "0");
  return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
}
const youtubeExtractor = {
  name: "youtube",
  canHandle(url) {
    try {
      const u = new URL(url);
      if (!isYoutubeHost(u.hostname)) return false;
      return isWatchUrl(u);
    } catch {
      return false;
    }
  },
  extract(html, _url) {
    if (!html) return null;
    const playerJson = extractJsonBlob(html, "ytInitialPlayerResponse");
    if (!playerJson || typeof playerJson !== "object") return null;
    const player = playerJson;
    const videoDetails = player.videoDetails ?? {};
    const microformat = player.microformat;
    const microRenderer = microformat?.playerMicroformatRenderer;
    const playabilityStatus = player.playabilityStatus;
    const status = toString(playabilityStatus?.status) || "OK";
    const isUnplayable = status !== "OK";
    const title = toString(videoDetails.title) || toString(microRenderer?.title);
    const videoId = toString(videoDetails.videoId);
    if (!videoId && !title) return null;
    const durationSeconds = toNumber(
      videoDetails.lengthSeconds ?? microRenderer?.lengthSeconds ?? 0
    );
    const duration = formatIsoDuration(durationSeconds);
    const channel = toString(videoDetails.author) || toString(microRenderer?.ownerChannelName);
    const description = toString(videoDetails.shortDescription) || toString(microRenderer?.description);
    const viewCount = toNumber(videoDetails.viewCount ?? microRenderer?.viewCount ?? 0);
    const postedAt = toString(microRenderer?.uploadDate) || toString(microRenderer?.publishDate);
    const captionTracks = isUnplayable ? [] : parseCaptionTracks(player);
    const initialData = extractJsonBlob(html, "ytInitialData");
    const chapters = isUnplayable ? [] : parseChapters(initialData);
    const youtubeMeta = {
      video_id: videoId,
      channel,
      duration,
      duration_seconds: durationSeconds,
      view_count: viewCount,
      posted_at: postedAt,
      chapters,
      caption_tracks: captionTracks,
      // Always empty — see header comment. Populated by an async caller, not here.
      transcript: [],
      playability_status: status
    };
    const markdown = buildMarkdown(youtubeMeta, title || videoId, description);
    const siteData = {
      video_id: youtubeMeta.video_id,
      channel: youtubeMeta.channel,
      duration: youtubeMeta.duration,
      duration_seconds: youtubeMeta.duration_seconds,
      view_count: youtubeMeta.view_count,
      posted_at: youtubeMeta.posted_at,
      chapters: youtubeMeta.chapters,
      caption_tracks: youtubeMeta.caption_tracks,
      transcript: youtubeMeta.transcript,
      playability_status: youtubeMeta.playability_status
    };
    const resolvedTitle = title || videoId;
    if (resolvedTitle) siteData.title = resolvedTitle;
    return {
      title: resolvedTitle,
      markdown,
      metadata: {
        ...siteData,
        description,
        author: channel,
        date: postedAt
      },
      links: [],
      images: [],
      extractor: "site-specific",
      site_data: siteData
    };
  }
};
export {
  youtubeExtractor
};
//# sourceMappingURL=youtube.js.map