import type { PlaylistData, PlaylistMetadata, PlaylistVideo, Thumbnail, } from "../types/playlist"; /* eslint-disable @typescript-eslint/no-explicit-any */ export interface InitialParseResult { metadata: PlaylistMetadata; videos: PlaylistVideo[]; } export function parseInitialData(raw: any): InitialParseResult | null { try { const metadata = extractMetadata(raw); if (!metadata) return null; const videoListContents = findVideoListContents(raw); if (!videoListContents) return null; const { videos } = parseVideoList(videoListContents); return { metadata, videos }; } catch (e) { console.error("[yt-playlist-features] Failed to parse playlist data:", e); return null; } } export function buildPlaylistData( metadata: PlaylistMetadata, videos: PlaylistVideo[], isComplete: boolean, ): PlaylistData { return { metadata, videos, extractedAt: new Date().toISOString(), isComplete, extractedCount: videos.length, }; } // --- internal helpers --- function parseVideoList(contents: any[]): { videos: PlaylistVideo[]; } { const videos: PlaylistVideo[] = []; for (const item of contents) { if (item.playlistVideoRenderer) { const video = parseVideo(item.playlistVideoRenderer); if (video) videos.push(video); } } return { videos }; } function extractMetadata(raw: any): PlaylistMetadata | null { const playlistId = extractPlaylistId(raw); if (!playlistId) return null; const metadataRenderer = raw?.metadata?.playlistMetadataRenderer; const title = metadataRenderer?.title ?? ""; const sidebarItems = raw?.sidebar?.playlistSidebarRenderer?.items ?? []; const primaryInfo = sidebarItems[0]?.playlistSidebarPrimaryInfoRenderer; const secondaryInfo = sidebarItems[1]?.playlistSidebarSecondaryInfoRenderer; const stats = primaryInfo?.stats ?? []; const videoCount = parseVideoCount(stats[0]); const viewCountText = extractText(stats[1]) || null; const lastUpdatedText = extractText(stats[2]) || null; const pageHeaderVM = raw?.header?.pageHeaderRenderer?.content?.pageHeaderViewModel; const description = extractText(pageHeaderVM?.description?.descriptionPreviewViewModel?.description) || extractText(primaryInfo?.description) || ""; const ownerRenderer = secondaryInfo?.videoOwner?.videoOwnerRenderer; const ownerRun = ownerRenderer?.title?.runs?.[0]; const ownerEndpoint = ownerRun?.navigationEndpoint?.browseEndpoint; const thumbnails = extractPlaylistThumbnails(pageHeaderVM, primaryInfo); return { playlistId, title, description, videoCount, totalDurationText: null, viewCountText, lastUpdatedText, thumbnails, owner: { name: ownerRun?.text ?? "", channelId: ownerEndpoint?.browseId ?? "", url: ownerRun?.navigationEndpoint?.commandMetadata ?.webCommandMetadata?.url ?? "", }, privacy: "unknown", }; } function extractPlaylistId(raw: any): string | null { const microformat = raw?.microformat?.microformatDataRenderer?.urlCanonical; if (microformat) { const match = microformat.match(/[?&]list=([^&]+)/); if (match) return match[1]; } const appLink = raw?.metadata?.playlistMetadataRenderer?.androidAppindexingLink; if (appLink) { const match = appLink.match(/[?&]list=([^&]+)/); if (match) return match[1]; } const url = new URL(window.location.href); return url.searchParams.get("list"); } function findVideoListContents(raw: any): any[] | null { const tabs = raw?.contents?.twoColumnBrowseResultsRenderer?.tabs; if (!tabs?.length) return null; const tabContent = tabs[0]?.tabRenderer?.content; const sectionContents = tabContent?.sectionListRenderer?.contents; if (!sectionContents?.length) return null; const itemSection = sectionContents[0]?.itemSectionRenderer?.contents; if (!itemSection?.length) return null; return itemSection[0]?.playlistVideoListRenderer?.contents ?? null; } function parseVideo(renderer: any): PlaylistVideo | null { const videoId = renderer.videoId; if (!videoId) return null; const bylineRun = renderer.shortBylineText?.runs?.[0]; const bylineEndpoint = bylineRun?.navigationEndpoint?.browseEndpoint; const lengthSeconds = renderer.lengthSeconds ? parseInt(renderer.lengthSeconds, 10) : null; return { videoId, title: extractText(renderer.title), index: parseInt(extractText(renderer.index) || "0", 10), durationSeconds: lengthSeconds, durationText: extractText(renderer.lengthText) || null, thumbnails: renderer.thumbnail?.thumbnails ?? [], channel: { name: bylineRun?.text ?? "", channelId: bylineEndpoint?.browseId ?? "", url: bylineRun?.navigationEndpoint?.commandMetadata ?.webCommandMetadata?.url ?? "", }, isPlayable: renderer.isPlayable !== false, isLive: renderer.badges?.some( (b: any) => b.metadataBadgeRenderer?.style === "BADGE_STYLE_TYPE_LIVE_NOW", ) ?? false, addedBy: null, voteCount: null, }; } function extractText(textObj: any): string { if (!textObj) return ""; if (typeof textObj === "string") return textObj; if (textObj.simpleText) return textObj.simpleText; if (textObj.runs) { return textObj.runs.map((r: any) => r.text).join(""); } if (textObj.content) return extractText(textObj.content); return ""; } function extractPlaylistThumbnails( pageHeaderVM: any, primaryInfo: any, ): Thumbnail[] { const heroThumbnails = pageHeaderVM?.heroImage?.contentPreviewImageViewModel?.image?.sources; if (heroThumbnails?.length) { return heroThumbnails.map((t: any) => ({ url: t.url ?? "", width: t.width ?? 0, height: t.height ?? 0, })); } return ( primaryInfo?.thumbnailRenderer?.playlistVideoThumbnailRenderer?.thumbnail ?.thumbnails ?? primaryInfo?.thumbnailRenderer?.playlistCustomThumbnailRenderer?.thumbnail ?.thumbnails ?? [] ); } function parseVideoCount(textObj: any): number { const text = extractText(textObj); const match = text.replace(/,/g, "").match(/(\d+)/); return match ? parseInt(match[1], 10) : 0; }