Korai Docs
Youtube

YouTube Integration Quick Start

Quick start guide for implementing YouTube features in your application

Quick Start Guide

This guide will help you quickly integrate YouTube functionality into your application using the Korai YouTube utilities.

Prerequisites

  1. YouTube Data API v3 Key

    • Visit Google Cloud Console
    • Create a new project or select existing
    • Enable YouTube Data API v3
    • Create credentials (API Key)
    • Restrict key to YouTube Data API v3
  2. Environment Setup

    # Add to .env.local
    YOUTUBE_API_KEY=AIzaSy...your_api_key
  3. Optional: Upstash Redis (for rate limiting)

    UPSTASH_REDIS_REST_URL=https://...
    UPSTASH_REDIS_REST_TOKEN=...

Installation

The YouTube utilities are already included in the project. No additional packages needed for basic functionality.

For transcript features, ensure you have:

npm install youtubei.js
# or
bun add youtubei.js

Basic Usage Examples

1. Fetch Video Details

import { extractVideoId, fetchVideoData } from '@/lib/ythelper';

async function example() {
  const url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
  
  // Extract video ID
  const videoId = extractVideoId(url);
  if (!videoId) {
    throw new Error('Invalid URL');
  }
  
  // Fetch video data
  const video = await fetchVideoData(videoId);
  
  console.log(video.title);
  console.log(video.viewsFormatted);
  console.log(video.durationFormatted);
}

2. Fetch Playlist Information

import { 
  extractPlaylistId, 
  fetchPlaylistDetails, 
  fetchPlaylistVideoIds,
  fetchVideoDetails 
} from '@/lib/ythelper';

async function example() {
  const url = 'https://www.youtube.com/playlist?list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf';
  
  // Extract playlist ID
  const playlistId = extractPlaylistId(url);
  
  // Fetch playlist details
  const playlist = await fetchPlaylistDetails(playlistId);
  
  // Fetch all video IDs
  const videoIds = await fetchPlaylistVideoIds(playlistId);
  
  // Fetch video details
  const { videos, totalDuration } = await fetchVideoDetails(videoIds);
  
  console.log(`${playlist.title}: ${videos.length} videos, ${totalDuration}s total`);
}

3. Fetch Channel Information

import { 
  extractChannelId, 
  fetchChannelData, 
  fetchRecentVideos 
} from '@/lib/ythelper';

async function example() {
  const channelId = 'UCXuqSBlHAE6Xw-yeJA0Tunw';
  
  // Fetch channel data
  const channel = await fetchChannelData(channelId);
  
  // Fetch recent videos
  const recentVideos = await fetchRecentVideos(channelId, 5);
  
  console.log(`${channel.name}: ${channel.subscribers} subscribers`);
  console.log(`Recent videos:`, recentVideos);
}

4. Extract Transcript

import { Innertube } from 'youtubei.js/web';
import { extractVideoId, fetchTranscript } from '@/lib/ythelper';

async function example() {
  const url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
  const videoId = extractVideoId(url);
  
  // Initialize Innertube
  const youtube = await Innertube.create({
    lang: 'en',
    location: 'US',
    retrieve_player: false
  });
  
  // Fetch transcript
  const transcript = await fetchTranscript(youtube, videoId);
  
  console.log('Full transcript:', transcript.fullTranscript);
  console.log('Segments:', transcript.segments.length);
}

API Endpoint Usage

Using the Video Detail Endpoint

// Client-side
async function getVideo(url: string) {
  const response = await fetch('/api/videoDetail', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ videoUrl: url })
  });
  
  const { video } = await response.json();
  return video;
}

Using the Playlist Endpoint

// Client-side
async function getPlaylist(url: string) {
  const params = new URLSearchParams({ id: url });
  const response = await fetch(`/api/playlist?${params}`);
  const data = await response.json();
  return data;
}

Using the Transcribe Endpoint

// Client-side
async function getTranscript(url: string) {
  const response = await fetch('/api/transcribe', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ videoUrl: url })
  });
  
  const { transcript, rateLimit } = await response.json();
  console.log(`Remaining: ${rateLimit.remaining}/${rateLimit.limit}`);
  return transcript;
}

React Component Examples

Simple Video Display

'use client';

import { useState } from 'react';
import { VideoData } from '@/lib/youtube';

export function VideoViewer() {
  const [url, setUrl] = useState('');
  const [video, setVideo] = useState<VideoData | null>(null);
  const [loading, setLoading] = useState(false);

  const fetchVideo = async () => {
    setLoading(true);
    try {
      const response = await fetch('/api/videoDetail', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ videoUrl: url })
      });
      const { video } = await response.json();
      setVideo(video);
    } catch (error) {
      console.error('Error:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      <input
        type="text"
        value={url}
        onChange={(e) => setUrl(e.target.value)}
        placeholder="YouTube URL"
      />
      <button onClick={fetchVideo} disabled={loading}>
        {loading ? 'Loading...' : 'Fetch Video'}
      </button>
      
      {video && (
        <div>
          <h2>{video.title}</h2>
          <p>{video.channel}</p>
          <p>{video.viewsFormatted}</p>
          <p>{video.durationFormatted}</p>
        </div>
      )}
    </div>
  );
}

Playlist Browser

'use client';

import { useState } from 'react';
import { PlaylistDetails, VideoItem } from '@/lib/youtube';

export function PlaylistBrowser() {
  const [url, setUrl] = useState('');
  const [data, setData] = useState<{
    playlist: PlaylistDetails;
    videos: VideoItem[];
  } | null>(null);

  const fetchPlaylist = async () => {
    const params = new URLSearchParams({ id: url });
    const response = await fetch(`/api/playlist?${params}`);
    const result = await response.json();
    setData({
      playlist: result.playlistDetails,
      videos: result.videos
    });
  };

  return (
    <div>
      <input
        type="text"
        value={url}
        onChange={(e) => setUrl(e.target.value)}
        placeholder="Playlist URL"
      />
      <button onClick={fetchPlaylist}>Load Playlist</button>
      
      {data && (
        <>
          <h2>{data.playlist.title}</h2>
          <p>{data.videos.length} videos</p>
          <ul>
            {data.videos.map((video) => (
              <li key={video.id}>{video.title}</li>
            ))}
          </ul>
        </>
      )}
    </div>
  );
}

Common Patterns

URL Validation Before API Call

import { validateYoutubeVideoUrl } from '@/lib/youtube-validator';

async function safeVideoFetch(url: string) {
  // Validate first
  const validation = validateYoutubeVideoUrl(url);
  
  if (!validation.isValid) {
    throw new Error(validation.error);
  }
  
  // Now safe to call API
  const response = await fetch('/api/videoDetail', {
    method: 'POST',
    body: JSON.stringify({ videoUrl: url })
  });
  
  return response.json();
}

Error Handling Pattern

async function robustVideoFetch(url: string) {
  try {
    const response = await fetch('/api/videoDetail', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ videoUrl: url })
    });
    
    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.error || 'Unknown error');
    }
    
    return await response.json();
  } catch (error) {
    if (error instanceof TypeError) {
      throw new Error('Network error - check your connection');
    }
    throw error;
  }
}

Loading States Pattern

import { useState } from 'react';

function useYouTubeVideo() {
  const [video, setVideo] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const fetchVideo = async (url: string) => {
    setLoading(true);
    setError('');
    setVideo(null);
    
    try {
      const response = await fetch('/api/videoDetail', {
        method: 'POST',
        body: JSON.stringify({ videoUrl: url })
      });
      
      if (!response.ok) {
        throw new Error('Failed to fetch video');
      }
      
      const { video } = await response.json();
      setVideo(video);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return { video, loading, error, fetchVideo };
}

Server Actions Example

'use server';

import { 
  extractVideoId, 
  fetchVideoData 
} from '@/lib/ythelper';

export async function getVideoAction(url: string) {
  const videoId = extractVideoId(url);
  
  if (!videoId) {
    return { 
      success: false, 
      error: 'Invalid YouTube URL' 
    };
  }
  
  try {
    const video = await fetchVideoData(videoId);
    return { 
      success: true, 
      data: video 
    };
  } catch (error) {
    return { 
      success: false, 
      error: error.message 
    };
  }
}

// Usage in client component
'use client';

import { getVideoAction } from './actions';

export function VideoForm() {
  const [url, setUrl] = useState('');
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    const result = await getVideoAction(url);
    
    if (result.success) {
      console.log(result.data);
    } else {
      console.error(result.error);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input value={url} onChange={(e) => setUrl(e.target.value)} />
      <button type="submit">Fetch</button>
    </form>
  );
}

Testing

Unit Test Example

import { extractVideoId, parseDuration, formatNumber } from '@/lib/ythelper';

describe('YouTube Helpers', () => {
  test('extractVideoId extracts from standard URL', () => {
    const url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
    expect(extractVideoId(url)).toBe('dQw4w9WgXcQ');
  });
  
  test('parseDuration converts ISO 8601 to seconds', () => {
    expect(parseDuration('PT1H23M45S')).toBe(5025);
    expect(parseDuration('PT15M')).toBe(900);
    expect(parseDuration('PT30S')).toBe(30);
  });
  
  test('formatNumber uses compact notation', () => {
    expect(formatNumber(1234)).toBe('1.2K');
    expect(formatNumber(1234567)).toBe('1.2M');
    expect(formatNumber(1234567890)).toBe('1.2B');
  });
});

Troubleshooting

Common Issues

Issue: "Invalid YouTube URL"

// Solution: Use URL validator first
import { validateYoutubeVideoUrl } from '@/lib/youtube-validator';

const validation = validateYoutubeVideoUrl(url);
if (!validation.isValid) {
  console.error(validation.error);
}

Issue: "Rate limit exceeded"

// Solution: Check rate limit headers and implement backoff
if (response.status === 429) {
  const reset = response.headers.get('X-RateLimit-Reset');
  const resetDate = new Date(parseInt(reset) * 1000);
  console.log(`Try again at ${resetDate}`);
}

Issue: "No transcript available"

// Solution: Check if video has captions
try {
  const transcript = await fetchTranscript(youtube, videoId);
} catch (error) {
  if (error.message.includes('No transcript available')) {
    console.log('This video does not have captions enabled');
  }
}

Next Steps

Resources