Docs/Skills and code/src/

src/video-info-fetch.js

Code111 lines4.6 KBsrc/

Mentioned on: page 12

import { bestThumbnailUrl, formatDurationText, parseAbsoluteDate, textValue } from "./video-normalization.js";

function firstText(...values) {
  for (const value of values) {
    const candidate = typeof value === "number" ? String(value) : textValue(value);
    if (candidate != null && candidate.trim()) return candidate.trim();
  }
  return null;
}

function numericOrNull(value) {
  if (value == null || value === "") return null;
  const number = Number(value);
  return Number.isFinite(number) ? number : null;
}

function safeJson(value) {
  try { return JSON.stringify(value); } catch { return null; }
}

/**
 * Normalize either youtubei.js getBasicInfo() or getInfo() into our stable DB shape.
 * getBasicInfo avoids parsing the watch-next response, where youtubei.js 18.1.0
 * currently logs a StructuredDescriptionContent / VideoTitleHeaderView type mismatch.
 */
export function normalizeVideoInfoPayload(info, videoId, source = "youtubei_get_basic_info") {
  const basic = info?.basic_info ?? {};
  const primary = info?.primary_info ?? {};
  const secondary = info?.secondary_info ?? {};
  const owner = secondary?.owner ?? {};
  const viewText = firstText(
    primary?.view_count?.view_count,
    primary?.view_count?.short_view_count,
    primary?.view_count?.original_view_count
  );
  const publishedText = firstText(
    basic.publish_date,
    basic.upload_date,
    basic.published_at,
    primary?.published,
    primary?.relative_date
  );
  const parsedDate = parseAbsoluteDate(publishedText);
  const rawViewCount = numericOrNull(basic.view_count) ?? numericOrNull(primary?.view_count?.original_view_count);
  const thumbnailList = basic.thumbnail ?? basic.thumbnails ?? [];
  const thumbnailUrl = bestThumbnailUrl(thumbnailList);
  const channelName = firstText(basic.author, owner?.title, owner?.author?.name);
  const normalizedId = String(basic.id ?? videoId);
  const result = {
    video_id: normalizedId,
    title: firstText(basic.title, primary?.title) ?? "",
    url: `https://www.youtube.com/watch?v=${normalizedId}`,
    channel_url: null,
    channel_name: channelName,
    video_channel_id: basic.channel_id ?? owner?.author?.id ?? null,
    thumbnail_url: thumbnailUrl,
    duration: formatDurationText(basic.duration),
    duration_seconds: numericOrNull(basic.duration),
    duration_precision: numericOrNull(basic.duration) == null ? "unknown" : "exact",
    view_count: rawViewCount,
    view_count_text: viewText ?? (rawViewCount == null ? null : String(rawViewCount)),
    view_count_precision: rawViewCount == null ? "unknown" : "exact",
    published_at: parsedDate.value,
    published_at_text: publishedText,
    published_at_precision: parsedDate.precision,
    likes_count: numericOrNull(basic.like_count),
    category: basic.category ?? null,
    description: basic.short_description ?? null,
    metadata_source: source,
    raw_metadata_json: safeJson({
      source,
      video_id: basic.id ?? videoId,
      channel_id: basic.channel_id ?? null,
      title: basic.title ?? null,
      duration: basic.duration ?? null,
      view_count: basic.view_count ?? null,
      like_count: basic.like_count ?? null,
      category: basic.category ?? null,
      publish_date: basic.publish_date ?? null,
      upload_date: basic.upload_date ?? null,
      published_text: publishedText,
      relative_date_text: textValue(primary?.relative_date),
      thumbnail_urls: Array.isArray(thumbnailList) ? thumbnailList.filter(item => item?.url).map(item => item.url) : [],
      playability_status: info?.playability_status?.status ?? null,
      basic_info_keys: Object.keys(basic),
      primary_info_available: Object.keys(primary).length > 0,
      secondary_info_available: Object.keys(secondary).length > 0
    })
  };

  if (!result.title) {
    const status = info?.playability_status?.status ?? "unknown";
    throw new Error(`YouTube returned no usable video title for ${videoId} (playability_status=${status}; source=${source}).`);
  }
  return result;
}

/**
 * Use the lighter player-only request by default. Opt into getInfo() only when
 * secondary/watch-next metadata is specifically needed: it is slower and can
 * trigger known parser mismatch warnings on YouTube's changing response shape.
 */
export async function fetchVideoInfo(youtube, videoId, { fullInfo = false } = {}) {
  const method = fullInfo ? "getInfo" : "getBasicInfo";
  if (typeof youtube?.[method] !== "function") {
    throw new Error(`youtubei.js client does not expose ${method}().`);
  }
  const info = await youtube[method](videoId);
  return normalizeVideoInfoPayload(info, videoId, fullInfo ? "youtubei_get_info" : "youtubei_get_basic_info");
}

Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.