diff --git a/src/app/components/url-preview/YouTubeEmbed.tsx b/src/app/components/url-preview/YouTubeEmbed.tsx index 88c4957..1e8f66f 100644 --- a/src/app/components/url-preview/YouTubeEmbed.tsx +++ b/src/app/components/url-preview/YouTubeEmbed.tsx @@ -1,4 +1,4 @@ -import React, { MouseEvent, useCallback, useState } from 'react'; +import React, { MouseEvent, useCallback, useEffect, useState } from 'react'; import { Box, Icon, Icons, Spinner, Text, as } from 'folds'; import * as css from './YouTubeEmbed.css'; import { getYouTubeStream, isYouTubeStreamingAvailable, openExternalUrl } from '../../utils/tauri'; @@ -24,6 +24,11 @@ const YOUTUBE_URL_PATTERNS = [ */ const YOUTUBE_EMBED_BASE = 'https://www.youtube.com/embed'; +/** + * YouTube oEmbed API base - used to fetch video metadata + */ +const YOUTUBE_OEMBED_API = 'https://www.youtube.com/oembed'; + /** * Get YouTube thumbnail URL for a video * @param videoId The YouTube video ID @@ -33,6 +38,23 @@ function getYouTubeThumbnailUrl(videoId: string): string { return `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`; } +/** + * Fetch YouTube video title using oEmbed API + * @param url The YouTube video URL + * @returns The video title or null if fetch fails + */ +async function fetchYouTubeTitle(url: string): Promise { + try { + const oembedUrl = `${YOUTUBE_OEMBED_API}?url=${encodeURIComponent(url)}&format=json`; + const response = await fetch(oembedUrl); + if (!response.ok) return null; + const data = await response.json(); + return data.title || null; + } catch { + return null; + } +} + /** * Checks if a URL is a YouTube URL * @param url The URL to check @@ -101,6 +123,22 @@ type YouTubeEmbedProps = { export const YouTubeEmbed = as<'div', YouTubeEmbedProps>(({ url, ...props }, ref) => { const videoId = extractYouTubeVideoId(url); const [playerState, setPlayerState] = useState({ status: 'thumbnail' }); + const [videoTitle, setVideoTitle] = useState(null); + + // Fetch video title on mount + useEffect(() => { + let cancelled = false; + + fetchYouTubeTitle(url).then((title) => { + if (!cancelled) { + setVideoTitle(title); + } + }); + + return () => { + cancelled = true; + }; + }, [url]); const handlePlay = useCallback(async () => { if (!isYouTubeStreamingAvailable()) { @@ -141,11 +179,19 @@ export const YouTubeEmbed = as<'div', YouTubeEmbedProps>(({ url, ...props }, ref ); + // Determine what to show in the header based on video title availability + const getHeaderLabel = (suffix?: string) => { + if (videoTitle) { + return suffix ? `${videoTitle} ${suffix}` : videoTitle; + } + return suffix ? `YouTube ${suffix}` : 'YouTube'; + }; + // Thumbnail preview - click to play if (playerState.status === 'thumbnail') { return ( - {renderHeader('YouTube')} + {renderHeader(getHeaderLabel())}