feat: add video title fetching from YouTube oEmbed API in YouTubeEmbed component

This commit is contained in:
2026-03-15 17:24:54 +11:00
parent 3cb4237f59
commit c980439bc9

View File

@@ -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 { Box, Icon, Icons, Spinner, Text, as } from 'folds';
import * as css from './YouTubeEmbed.css'; import * as css from './YouTubeEmbed.css';
import { getYouTubeStream, isYouTubeStreamingAvailable, openExternalUrl } from '../../utils/tauri'; import { getYouTubeStream, isYouTubeStreamingAvailable, openExternalUrl } from '../../utils/tauri';
@@ -24,6 +24,11 @@ const YOUTUBE_URL_PATTERNS = [
*/ */
const YOUTUBE_EMBED_BASE = 'https://www.youtube.com/embed'; 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 * Get YouTube thumbnail URL for a video
* @param videoId The YouTube video ID * @param videoId The YouTube video ID
@@ -33,6 +38,23 @@ function getYouTubeThumbnailUrl(videoId: string): string {
return `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`; 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<string | null> {
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 * Checks if a URL is a YouTube URL
* @param url The URL to check * @param url The URL to check
@@ -101,6 +123,22 @@ type YouTubeEmbedProps = {
export const YouTubeEmbed = as<'div', YouTubeEmbedProps>(({ url, ...props }, ref) => { export const YouTubeEmbed = as<'div', YouTubeEmbedProps>(({ url, ...props }, ref) => {
const videoId = extractYouTubeVideoId(url); const videoId = extractYouTubeVideoId(url);
const [playerState, setPlayerState] = useState<PlayerState>({ status: 'thumbnail' }); const [playerState, setPlayerState] = useState<PlayerState>({ status: 'thumbnail' });
const [videoTitle, setVideoTitle] = useState<string | null>(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 () => { const handlePlay = useCallback(async () => {
if (!isYouTubeStreamingAvailable()) { if (!isYouTubeStreamingAvailable()) {
@@ -141,11 +179,19 @@ export const YouTubeEmbed = as<'div', YouTubeEmbedProps>(({ url, ...props }, ref
</div> </div>
); );
// 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 // Thumbnail preview - click to play
if (playerState.status === 'thumbnail') { if (playerState.status === 'thumbnail') {
return ( return (
<Box shrink="No" className={css.YouTubeEmbed} direction="Column" {...props} ref={ref}> <Box shrink="No" className={css.YouTubeEmbed} direction="Column" {...props} ref={ref}>
{renderHeader('YouTube')} {renderHeader(getHeaderLabel())}
<button <button
type="button" type="button"
className={css.YouTubeThumbnailButton} className={css.YouTubeThumbnailButton}
@@ -169,7 +215,7 @@ export const YouTubeEmbed = as<'div', YouTubeEmbedProps>(({ url, ...props }, ref
if (playerState.status === 'loading') { if (playerState.status === 'loading') {
return ( return (
<Box shrink="No" className={css.YouTubeEmbed} direction="Column" {...props} ref={ref}> <Box shrink="No" className={css.YouTubeEmbed} direction="Column" {...props} ref={ref}>
{renderHeader('YouTube - Loading...')} {renderHeader(getHeaderLabel('- Loading...'))}
<Box <Box
className={css.YouTubeEmbedIframe} className={css.YouTubeEmbedIframe}
alignItems="Center" alignItems="Center"
@@ -204,7 +250,7 @@ export const YouTubeEmbed = as<'div', YouTubeEmbedProps>(({ url, ...props }, ref
const embedUrl = getYouTubeEmbedUrl(videoId); const embedUrl = getYouTubeEmbedUrl(videoId);
return ( return (
<Box shrink="No" className={css.YouTubeEmbed} direction="Column" {...props} ref={ref}> <Box shrink="No" className={css.YouTubeEmbed} direction="Column" {...props} ref={ref}>
{renderHeader('YouTube')} {renderHeader(getHeaderLabel())}
<iframe <iframe
className={css.YouTubeEmbedIframe} className={css.YouTubeEmbedIframe}
src={embedUrl} src={embedUrl}