import Parser from "rss-parser"; import type { ContentItem } from "../content/types"; type YoutubeApiVideo = { id: string; url: string; title: string; summary?: string; publishedAt: string; thumbnailUrl?: string; views?: number; }; function stripHtml(s: string) { return (s || "") .replace(/<[^>]+>/g, " ") .replace(/\s+/g, " ") .trim(); } function truncate(s: string, n: number) { const t = stripHtml(s); if (!t) return ""; if (t.length <= n) return t; return `${t.slice(0, Math.max(0, n - 1)).trimEnd()}…`; } export async function fetchYoutubeViaRss(channelId: string, limit = 20): Promise { const feedUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${encodeURIComponent(channelId)}`; const parser = new Parser(); const feed = await parser.parseURL(feedUrl); return normalizeYoutubeRssFeedItems(feed.items || [], limit); } async function youtubeApiGetJson(url: string): Promise { const res = await fetch(url); if (!res.ok) throw new Error(`YouTube API request failed: ${res.status} ${res.statusText}`); return (await res.json()) as T; } export function normalizeYoutubeRssFeedItems(items: any[], limit: number): ContentItem[] { return (items || []) .slice(0, limit) .map((it) => { const url = it.link || ""; const id = (it.id || url).toString(); const publishedAt = (it.isoDate || it.pubDate || new Date(0).toISOString()).toString(); const summary = truncate( (it.contentSnippet || it.summary || it.content || it["content:encoded"] || "").toString(), 240, ); return { id, source: "youtube" as const, url, title: (it.title || "").toString(), summary: summary || undefined, publishedAt: new Date(publishedAt).toISOString(), thumbnailUrl: (it.enclosure?.url || undefined) as string | undefined, }; }) .filter((x) => x.url && x.title); } export function normalizeYoutubeApiVideos( items: Array<{ id: string; snippet: { title: string; description?: string; publishedAt: string; thumbnails?: Record; }; statistics?: { viewCount?: string }; }>, ): ContentItem[] { const normalized: YoutubeApiVideo[] = (items || []).map((v) => ({ id: v.id, url: `https://www.youtube.com/watch?v=${encodeURIComponent(v.id)}`, title: v.snippet.title, summary: v.snippet.description ? truncate(v.snippet.description, 240) : undefined, publishedAt: new Date(v.snippet.publishedAt).toISOString(), thumbnailUrl: v.snippet.thumbnails?.high?.url || v.snippet.thumbnails?.default?.url, views: v.statistics?.viewCount ? Number(v.statistics.viewCount) : undefined, })); return normalized.map((v) => ({ id: v.id, source: "youtube", url: v.url, title: v.title, summary: v.summary, publishedAt: v.publishedAt, thumbnailUrl: v.thumbnailUrl, metrics: v.views !== undefined ? { views: v.views } : undefined, })); } export async function fetchYoutubeViaApi( channelId: string, apiKey: string, limit = 20, ): Promise { // 1) Get latest video IDs from channel. const searchUrl = "https://www.googleapis.com/youtube/v3/search" + `?part=snippet&channelId=${encodeURIComponent(channelId)}` + `&maxResults=${encodeURIComponent(String(limit))}` + `&order=date&type=video&key=${encodeURIComponent(apiKey)}`; const search = await youtubeApiGetJson<{ items: Array<{ id: { videoId: string }; snippet: { title: string; publishedAt: string; thumbnails?: any }; }>; }>(searchUrl); const videoIds = (search.items || []).map((x) => x.id.videoId).filter(Boolean); if (videoIds.length === 0) return []; // 2) Fetch statistics. const videosUrl = "https://www.googleapis.com/youtube/v3/videos" + `?part=snippet,statistics&maxResults=${encodeURIComponent(String(videoIds.length))}` + `&id=${encodeURIComponent(videoIds.join(","))}` + `&key=${encodeURIComponent(apiKey)}`; const videos = await youtubeApiGetJson<{ items: Array<{ id: string; snippet: { title: string; publishedAt: string; thumbnails?: Record }; statistics?: { viewCount?: string }; }>; }>(videosUrl); return normalizeYoutubeApiVideos(videos.items || []); }