A powerful Next.js application that syncs your YouTube saved videos (Watch Later, Liked Videos, or custom playlists) to a local MySQL database and provides lightning-fast keyword search across titles, descriptions, and transcripts.
- YouTube Integration: Fetch videos from Watch Later, Liked Videos, or any playlist
- Full-Text Search: Search across video titles, descriptions, and transcripts using MySQL FULLTEXT indexes
- Transcript Support: Optional automatic transcript fetching for enhanced searchability
- Modern UI: Clean, responsive interface built with Next.js 16 and Tailwind CSS
- Real-time Sync: One-click synchronization of your YouTube videos
- Type-Safe: Built with TypeScript throughout
- Frontend: Next.js 16 (App Router), React 19, TypeScript, Tailwind CSS
- Backend: Next.js API Routes, Prisma ORM
- Database: MySQL 8.0+
- External APIs: YouTube Data API v3, youtube-transcript
- Node.js 18+ and npm/yarn/pnpm
- MySQL 8.0+ database
- YouTube Data API credentials (API Key or OAuth 2.0)
cd yousearch
npm installCreate a MySQL database for the application:
CREATE DATABASE yousearch CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;Copy the example environment file:
cp .env.example .envEdit .env with your configuration:
# Database
DATABASE_URL="mysql://username:password@localhost:3306/yousearch"
# YouTube API - Choose ONE option:
# Option 1: API Key (for public playlists only)
YOUTUBE_API_KEY="your_youtube_api_key_here"
# Option 2: OAuth 2.0 (required for Watch Later/Liked Videos)
GOOGLE_CLIENT_ID="your_client_id.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="your_client_secret"
YOUTUBE_ACCESS_TOKEN="obtained_after_oauth_flow"
YOUTUBE_REFRESH_TOKEN="obtained_after_oauth_flow"- Go to Google Cloud Console
- Create a new project or select existing one
- Enable YouTube Data API v3
- Go to Credentials → Create Credentials → API Key
- Copy the API key to your
.envfile
Limitations: API keys can only access public data. To sync Watch Later or Liked Videos, you need OAuth.
- Go to Google Cloud Console
- Create a new project or select existing one
- Enable YouTube Data API v3
- Go to Credentials → Create Credentials → OAuth 2.0 Client ID
- Configure OAuth consent screen:
- User Type: External
- Add scopes:
https://www.googleapis.com/auth/youtube.readonly
- Create OAuth Client ID:
- Application Type: Web application
- Authorized redirect URIs:
http://localhost:3000/api/auth/callback
- Download credentials and add to
.env
Getting Access Token:
You can use the OAuth 2.0 Playground to get tokens:
- Go to OAuth 2.0 Playground
- Click settings (gear icon) → Use your own OAuth credentials
- Enter your Client ID and Client Secret
- In Step 1, select YouTube Data API v3 →
https://www.googleapis.com/auth/youtube.readonly - Click "Authorize APIs" and sign in
- In Step 2, click "Exchange authorization code for tokens"
- Copy the
access_tokenandrefresh_tokento your.envfile
Generate Prisma client and create database tables:
npx prisma generate
npx prisma db pushTo view your database in Prisma Studio:
npx prisma studioInstall the youtube-transcript package for transcript fetching:
npm install youtube-transcriptInstall Lucide icons for UI:
npm install lucide-reactnpm run devOpen http://localhost:3000 in your browser.
- Click the "Sync Videos" button in the top-right corner
- The app will fetch videos from your Watch Later playlist (default)
- Videos are stored in MySQL with full metadata and transcripts
Customize sync behavior by modifying the request in components/SyncButton.tsx:
{
source: 'watchLater', // or 'liked' or 'playlist'
playlistId: 'PLxxxxxx', // optional, for custom playlists
fetchTranscripts: true, // fetch transcripts (slower)
maxVideos: 100, // limit number of videos
updateExisting: true // update already-synced videos
}- Type keywords in the search box
- Results show matching videos from titles, descriptions, and transcripts
- Click video titles to open on YouTube
- Transcript snippets show where keywords were found
Synchronize YouTube videos to database.
Request Body:
{
"source": "watchLater",
"playlistId": "optional-playlist-id",
"fetchTranscripts": true,
"maxVideos": 100,
"updateExisting": true
}Response:
{
"success": true,
"videosAdded": 45,
"videosUpdated": 12,
"errors": [],
"duration": 23000
}Search videos by keyword.
Query Parameters:
q(required): Search querypage: Page number (default: 1)limit: Results per page (default: 20, max: 100)sortBy: Sort order -relevance,date, ortitle(default:relevance)channel: Filter by channel name
Example:
GET /api/videos/search?q=typescript&page=1&limit=20&sortBy=relevance
Response:
{
"results": [
{
"id": "...",
"youtubeId": "dQw4w9WgXcQ",
"title": "Video Title",
"description": "Video description...",
"channelTitle": "Channel Name",
"publishedAt": "2024-01-15T12:00:00.000Z",
"thumbnailUrl": "https://...",
"duration": "PT10M30S",
"snippets": {
"title": "Video **Title**",
"transcript": "...found **keyword** here..."
}
}
],
"total": 156,
"page": 1,
"limit": 20,
"hasMore": true
}Get sync history.
Response:
{
"logs": [
{
"id": "...",
"type": "watchLater",
"status": "success",
"videosAdded": 45,
"videosUpdated": 12,
"startedAt": "2024-01-15T12:00:00.000Z",
"completedAt": "2024-01-15T12:05:00.000Z"
}
]
}model Video {
id String @id @default(cuid())
youtubeId String @unique
title String
description String?
channelTitle String
tags String? // JSON array
publishedAt DateTime
transcript String? // Full transcript text
thumbnailUrl String?
duration String? // ISO 8601 format
viewCount Int?
likeCount Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@fulltext([title, description, transcript])
}The app uses MySQL FULLTEXT indexes for fast searching:
- Combined index:
title,description,transcript - Individual indexes on each field for targeted searches
- FULLTEXT Search: Uses MySQL's native FULLTEXT search with BOOLEAN MODE for fast keyword matching
- Fallback: Gracefully falls back to LIKE-based search if FULLTEXT fails
- Indexes: B-tree indexes on
publishedAt,channelTitlefor filtering and sorting
- Batching: Fetches video details in batches of 50 (YouTube API limit)
- Pagination: Handles large playlists with automatic pagination
- Error Handling: Continues syncing even if individual videos fail
- Logging: Tracks sync history in
sync_logstable
- Optional: Can be disabled for faster syncing
- Caching: Transcripts are stored and not re-fetched unless video is updated
- Async: Transcripts are fetched asynchronously to avoid blocking
- Check that YouTube Data API v3 is enabled in Google Cloud Console
- Verify your API key or OAuth credentials are correct
- For OAuth: Ensure you have the
youtube.readonlyscope
- Watch Later requires OAuth authentication (API key won't work)
- Make sure
YOUTUBE_ACCESS_TOKENis set in.env - Try re-generating your OAuth tokens
- Run a sync first to populate the database
- Check that videos were successfully synced:
npx prisma studio - Try a broader search term
- Ensure MySQL version is 8.0+
- Check that FULLTEXT indexes were created:
npx prisma db push - MySQL requires at least 3 characters for FULLTEXT search by default
- Some videos don't have captions available
- Some videos have auto-generated captions that may fail to fetch
- YouTube may rate-limit transcript requests
- Set
fetchTranscripts: falsein sync request to skip
Update .env for production:
DATABASE_URL="mysql://user:pass@prod-host:3306/yousearch?sslmode=require"
NODE_ENV="production"
NEXT_PUBLIC_APP_URL="https://your-domain.com"- Use a managed MySQL service (AWS RDS, PlanetScale, etc.)
- Enable SSL connections
- Set up automated backups
- Monitor query performance
vercel --prodAdd environment variables in Vercel dashboard.
The app works on any platform that supports Next.js:
- Railway: Supports MySQL and Next.js
- Fly.io: Deploy with Docker
- AWS: Use Elastic Beanstalk or ECS
- DigitalOcean: App Platform with managed MySQL
MIT
Contributions welcome! Please open an issue or PR.
For issues or questions:
- Check existing issues on GitHub
- Review YouTube API documentation
- Check Prisma documentation for database issues