Study Sync integrates with YouTube Data API v3 to automatically fetch video metadata, including titles, durations, and thumbnails for study materials.
Overview
The YouTube integration allows users to:
- Add individual videos by URL
- Import entire playlists
- Automatically extract video metadata (title, duration, thumbnail)
- Track progress on video-based study materials
Configuration file: src/lib/youtube.js
Prerequisites
- Google Cloud Platform account
- YouTube Data API v3 enabled
Setup Instructions
Create Google Cloud Project
- Go to Google Cloud Console
- Click the project dropdown at the top
- Click “New Project”
- Enter project name (e.g., “Study Sync”)
- Click “Create”
Enable YouTube Data API v3
- In your project, navigate to APIs & Services > Library
- Search for “YouTube Data API v3”
- Click on YouTube Data API v3
- Click “Enable”
You must enable the API before creating credentials, otherwise the API key won’t work.
Create API Key
- Navigate to APIs & Services > Credentials
- Click “Create Credentials” > “API key”
- A dialog will show your new API key
- Click “Edit API key” to configure restrictions
Restrict API Key (Recommended)
For security, restrict your API key:API Restrictions:
- Select “Restrict key”
- Choose “YouTube Data API v3” from the dropdown
- Save
Application Restrictions (Production):
- Choose “HTTP referrers (web sites)”
- Add your production domain:
https://yourdomain.com/*
- For development, also add:
http://localhost:3000/*
- Save
Configure Environment Variable
Add the API key to your .env.local file:YOUTUBE_API_KEY=AIzaSyD1234567890abcdefghijklmnopqrstuv
This is a server-side variable and should NOT have the NEXT_PUBLIC_ prefix.
API Implementation
The YouTube API service is implemented in src/lib/youtube.js:
import { google } from "googleapis";
/**
* YouTube API Service for Next.js
* Handles fetching video and playlist metadata from YouTube Data API v3
*/
const youtube = google.youtube({
version: "v3",
auth: process.env.YOUTUBE_API_KEY,
});
The API extracts video IDs from various YouTube URL formats:
const patterns = [
/(?:https?:\/\/)?(?:www\.)?youtube\.com\/watch\?v=([^&\n?#]+)/,
/(?:https?:\/\/)?(?:www\.)?youtu\.be\/([^&\n?#]+)/,
/(?:https?:\/\/)?(?:www\.)?youtube\.com\/embed\/([^&\n?#]+)/,
/(?:https?:\/\/)?(?:www\.)?youtube\.com\/v\/([^&\n?#]+)/,
/(?:https?:\/\/)?(?:www\.)?youtube\.com\/shorts\/([^&\n?#]+)/,
/^([a-zA-Z0-9_-]{11})$/, // Direct video ID
];
Examples of Supported URLs
| Format | Example URL |
|---|
| Standard | https://www.youtube.com/watch?v=dQw4w9WgXcQ |
| Short URL | https://youtu.be/dQw4w9WgXcQ |
| Embed | https://www.youtube.com/embed/dQw4w9WgXcQ |
| Shorts | https://www.youtube.com/shorts/dQw4w9WgXcQ |
| Video ID | dQw4w9WgXcQ |
| Playlist | https://www.youtube.com/playlist?list=PLxxxxxxxxxxxxxx |
API Functions
Fetches metadata for a single video:
export const getVideoMetadata = async (url) => {
try {
const videoId = extractVideoId(url);
if (!videoId) {
throw new Error("Invalid YouTube video URL");
}
const response = await youtube.videos.list({
part: ["snippet", "contentDetails"],
id: [videoId],
});
if (!response.data.items || response.data.items.length === 0) {
throw new Error("Video not found");
}
const video = response.data.items[0];
const duration = parseDuration(video.contentDetails.duration);
return {
videoId: video.id,
title: video.snippet.title,
duration: Math.round(duration),
thumbnailUrl:
video.snippet.thumbnails?.medium?.url ||
video.snippet.thumbnails?.default?.url ||
"",
url: `https://www.youtube.com/watch?v=${video.id}`,
};
} catch (error) {
console.error("YouTube API error (video):", error.message);
throw new Error(`Failed to fetch video metadata: ${error.message}`);
}
};
Returns:
{
videoId: "dQw4w9WgXcQ",
title: "Video Title",
duration: 213, // in minutes
thumbnailUrl: "https://i.ytimg.com/vi/...",
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
}
Get Playlist Videos
Fetches all videos from a playlist:
export const getPlaylistVideos = async (url) => {
try {
const playlistId = extractPlaylistId(url);
if (!playlistId) {
throw new Error("Invalid YouTube playlist URL");
}
const videos = [];
let nextPageToken = null;
do {
const response = await youtube.playlistItems.list({
part: ["contentDetails"],
playlistId: playlistId,
maxResults: 50,
pageToken: nextPageToken,
});
const videoIds = response.data.items.map(
(item) => item.contentDetails.videoId
);
const videosResponse = await youtube.videos.list({
part: ["snippet", "contentDetails"],
id: videoIds,
});
for (const video of videosResponse.data.items) {
const duration = parseDuration(video.contentDetails.duration);
videos.push({
videoId: video.id,
title: video.snippet.title,
duration: Math.round(duration),
thumbnailUrl:
video.snippet.thumbnails?.medium?.url ||
video.snippet.thumbnails?.default?.url ||
"",
url: `https://www.youtube.com/watch?v=${video.id}`,
});
}
nextPageToken = response.data.nextPageToken;
} while (nextPageToken);
return videos;
} catch (error) {
console.error("YouTube API error (playlist):", error.message);
throw new Error(`Failed to fetch playlist videos: ${error.message}`);
}
};
Features:
- Handles pagination automatically
- Fetches up to 50 videos per request
- Continues until all videos are retrieved
Duration Parsing
Converts ISO 8601 duration format to minutes:
const parseDuration = (isoDuration) => {
const match = isoDuration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
if (!match) return 0;
const hours = parseInt(match[1] || 0);
const minutes = parseInt(match[2] || 0);
const seconds = parseInt(match[3] || 0);
return hours * 60 + minutes + seconds / 60;
};
Examples:
PT15M33S → 15.55 minutes
PT1H30M → 90 minutes
PT45S → 0.75 minutes
Usage Example
Using the YouTube API in a Next.js API route:
import { getVideoMetadata, getPlaylistVideos } from '@/lib/youtube';
export default async function handler(req, res) {
const { url, type } = req.body;
try {
if (type === 'video') {
const metadata = await getVideoMetadata(url);
res.json({ success: true, data: metadata });
} else if (type === 'playlist') {
const videos = await getPlaylistVideos(url);
res.json({ success: true, data: videos, count: videos.length });
} else {
res.status(400).json({ error: 'Invalid type' });
}
} catch (error) {
res.status(500).json({ error: error.message });
}
}
API Quota Management
YouTube Data API v3 has daily quota limits:
Default Quota
- Daily quota: 10,000 units per day (free tier)
- Video metadata request: ~3 units
- Playlist items request: ~3 units per page (50 videos)
Cost Examples
| Operation | Quota Cost | Daily Limit |
|---|
| Fetch 1 video | ~3 units | ~3,333 videos |
| Fetch 50-video playlist | ~6 units | ~1,666 playlists |
| Fetch 500-video playlist | ~33 units | ~303 playlists |
Monitor Quota Usage
- Go to Google Cloud Console
- Navigate to APIs & Services > Dashboard
- Click on YouTube Data API v3
- View Quotas tab for current usage
Request Quota Increase
If you need higher limits:
- Go to APIs & Services > YouTube Data API v3 > Quotas
- Click “Apply for higher quota”
- Fill out the quota increase request form
- Explain your use case and expected usage
Monitor your quota usage regularly to avoid hitting limits during peak usage.
Error Handling
Common errors and solutions:
“Invalid YouTube video URL”
Cause: URL format not recognized or invalid video ID.
Solution: Verify the URL matches one of the supported formats.
”Video not found”
Cause: Video is private, deleted, or ID is incorrect.
Solution: Ensure the video is public and the URL is correct.
”Invalid YouTube playlist URL”
Cause: Playlist URL doesn’t contain a valid list parameter.
Solution: Ensure URL includes ?list= parameter.
”API key not valid”
Cause: Invalid API key or API not enabled.
Solutions:
- Verify
YOUTUBE_API_KEY in .env.local
- Ensure YouTube Data API v3 is enabled in Google Cloud Console
- Check API key restrictions aren’t blocking requests
”Quota exceeded”
Cause: Daily quota limit reached.
Solutions:
- Wait until quota resets (midnight Pacific Time)
- Request quota increase from Google
- Implement caching to reduce API calls
Optimization Tips
1. Cache Results
Store fetched metadata to avoid repeated API calls:
// Store in MongoDB when first fetched
const metadata = await getVideoMetadata(url);
await db.collection('videoCache').insertOne({
videoId: metadata.videoId,
metadata,
cachedAt: new Date()
});
// Check cache before API call
const cached = await db.collection('videoCache')
.findOne({ videoId });
if (cached) return cached.metadata;
2. Batch Requests
The API supports fetching multiple videos in one request:
const response = await youtube.videos.list({
part: ["snippet", "contentDetails"],
id: ["videoId1", "videoId2", "videoId3"], // Up to 50 IDs
});
3. Use Minimal Parts
Only request needed data parts to reduce quota usage:
// Good - only request what you need
part: ["snippet", "contentDetails"]
// Avoid - requesting unnecessary data
part: ["snippet", "contentDetails", "statistics", "status"]
Security Best Practices
- Restrict API key: Limit to YouTube Data API v3 only
- Add referrer restrictions: Limit to your domain
- Server-side only: Never expose API key in client code
- Monitor usage: Set up quota alerts in Google Cloud
- Rotate keys: Change API keys periodically
- Use environment variables: Never hardcode API keys
Testing
import { getVideoMetadata } from '@/lib/youtube';
const testVideo = async () => {
try {
const metadata = await getVideoMetadata('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
console.log('✅ Video metadata:', metadata);
} catch (error) {
console.error('❌ Error:', error.message);
}
};
Test Playlist Import
import { getPlaylistVideos } from '@/lib/youtube';
const testPlaylist = async () => {
try {
const videos = await getPlaylistVideos('https://www.youtube.com/playlist?list=PLxxxxxx');
console.log(`✅ Fetched ${videos.length} videos from playlist`);
} catch (error) {
console.error('❌ Error:', error.message);
}
};
Resources