feat: add video title fetching from YouTube oEmbed API in YouTubeEmbed component
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user