A sleek web application that transforms YouTube playlists into a clean, focused experience. Track your progress, take markdown notes per video, and navigate seamlessly with keyboard shortcuts.
🌐 Live Demo: void-list.vercel.app
- Direct URL Input: Paste YouTube playlist URLs directly - no need to extract playlist IDs
- Progress Tracking: Visual progress bars and completion tracking for each playlist
- Per-Video Notes: Write and view markdown-formatted notes for each video with tabbed interface
- Keyboard Shortcuts: Navigate efficiently with keyboard controls
- Local Storage: All data is stored locally in your browser (no external database)
- Framework: Next.js 15.5.2 with App Router
- Language: TypeScript
- Markdown: ReactMarkdown for note rendering
- Data: Local browser storage (no external dependencies)
- Main landing page with playlist input and management
- Core Logic:
- URL parsing with regex patterns to extract playlist IDs from various YouTube URL formats
- React state management for playlists, search filtering, and loading states
- Local storage integration through cache system
- API calls to
/api/playlistendpoint for fetching playlist data
- Individual playlist interface with video player and notes
- Core Logic:
- Dynamic route parameter extraction using Next.js App Router
- YouTube iframe URL construction with playlist and video IDs
- Real-time note saving with debounced auto-save (500ms delay)
- Keyboard event handling with focus detection to prevent conflicts
- Progress state management and persistence to local storage
- YouTube playlist scraping endpoint
- Scraping Algorithm:
- Constructs YouTube playlist URL with optional continuation token
- Sends HTTP request with browser User-Agent to bypass bot detection
- Parses HTML response using multiple regex patterns for robustness
- Extracts video IDs using pattern:
"videoId":"([a-zA-Z0-9_-]{11})" - Extracts titles from JSON embedded in HTML using multiple fallback patterns
- Handles pagination by finding continuation tokens in HTML
- Implements 100ms delays between requests for rate limiting
- Deduplicates video IDs and titles across pages
- Returns structured JSON with all extracted data
- Local storage management for playlist data
- Storage Implementation:
interface PlaylistData { id: string; title: string; videoIds: string[]; titles: string[]; completedVideos: string[]; currentVideo: string | null; lastAccessed: number; createdAt: number; videoNotes: Record<string, VideoNote>; }
- Core Methods:
savePlaylist(): Serializes data to JSON and stores in localStoragegetPlaylist(): Deserializes JSON from localStorage with error handlinggetRecentPlaylists(): Retrieves all playlists sorted by lastAccessed timestampupdateVideoNote(): Updates specific video note and persists entire playlistclearAll(): Removes all playlist data from localStorage
- PlaylistCard: Calculates progress percentage, formats dates, handles click events
- Logo: Simple SVG component with configurable size props
- Component Logic: Event handling, state management, and data formatting
const extractPlaylistId = (url: string): string | null => {
const patterns = [
/[?&]list=([a-zA-Z0-9_-]+)/, // Standard YouTube URLs with list parameter
/playlist\?list=([a-zA-Z0-9_-]+)/, // Direct playlist URLs
/^([a-zA-Z0-9_-]+)$/ // Raw playlist ID input
];
for (const pattern of patterns) {
const match = url.match(pattern);
if (match) return match[1];
}
return null;
}// Multiple regex patterns for robust data extraction
const videoIdRegex = /"videoId":"([a-zA-Z0-9_-]{11})"/g;
const titlePatterns = [
/"title":{"runs":\[{"text":"([^"]+)"/g,
/"title":"([^"]+)"/g,
/"videoTitle":"([^"]+)"/g
];
// Pagination handling
const continuationRegex = /"continuationCommand":{"token":"([^"]+)"/;
const continuationMatch = continuationRegex.exec(html);
continuationToken = continuationMatch ? continuationMatch[1] : '';useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const target = e.target as HTMLElement;
// Prevent conflicts when typing in textarea
if (target.tagName === 'TEXTAREA') return;
if (e.code === 'Space') {
e.preventDefault();
toggleVideoCompleted();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [currentVideoId]);const debouncedSave = useCallback(
debounce((content: string) => {
playlistCache.updateVideoNote(playlistId, currentVideoId, {
content,
lastModified: Date.now()
});
}, 500),
[playlistId, currentVideoId]
);try {
const response = await fetch(playlistUrl, { headers: { 'User-Agent': '...' } });
if (!response.ok) {
return NextResponse.json(
{ error: 'Failed to fetch playlist page' },
{ status: response.status }
);
}
} catch (error) {
console.error('Failed to scrape playlist:', error);
return NextResponse.json(
{ error: 'Failed to scrape playlist' },
{ status: 500 }
);
}- Playlist ID validation using regex patterns
- Video ID format validation (11-character alphanumeric)
- Title length validation (max 200 characters)
- Array length synchronization between video IDs and titles
- Safe JSON parsing with fallback handling for corrupted localStorage data
-
Input Processing:
- User input validated and parsed using regex patterns
- Playlist ID extracted from various URL formats
- Input sanitization and error handling
-
API Request Cycle:
- GET request to
/api/playlist?playlistId=<id> - Server-side scraping with pagination loops
- Rate limiting and continuation token handling
- JSON response with structured data
- GET request to
-
Data Transformation:
- Raw HTML parsing with multiple regex patterns
- Video ID and title extraction and deduplication
- Array synchronization and padding for missing data
- Playlist metadata extraction and cleaning
-
State Management:
- React useState hooks for component state
- useEffect hooks for side effects and data fetching
- useCallback hooks for memoized functions
- Event handlers for user interactions
-
Persistence Layer:
- JSON serialization of playlist data
- localStorage API for browser storage
- Timestamp tracking for access patterns
- Atomic updates for data consistency
- YouTube iframe prevents programmatic video control
- Solution: User-initiated controls only, no automated play/pause
- Keyboard shortcuts work at document level, not iframe level
if (continuationToken && pageCount < maxPages) {
await new Promise(resolve => setTimeout(resolve, 100));
}- 100ms delays between pagination requests
- Maximum 20 pages limit to prevent infinite loops
- Browser User-Agent headers to avoid bot detection
- Deduplication algorithms for video IDs across pages
- Array length synchronization between IDs and titles
- Fallback title generation for missing data
- Safe JSON parsing with error recovery
- Node.js 18+
- npm, yarn, pnpm, or bun
# Install dependencies
npm install
# Run development server
npm run dev
# Build for production
npm run build
# Start production server
npm startOpen http://localhost:3000 to view the application.
- Add Playlist: Paste any YouTube playlist URL into the input field
- Navigate: Click on playlist cards to start watching
- Take Notes: Use the Notes tab to write markdown-formatted notes for each video
- Track Progress: Videos are automatically marked as completed when you navigate to the next one
- Search: Use the search bar to filter through your saved playlists
- Client-Side: React component memoization and debounced auto-save
- Server-Side: Efficient regex patterns and minimal DOM parsing
- Storage: JSON serialization optimized for localStorage limits
- Network: Respectful rate limiting and error recovery mechanisms
- No external databases or cloud storage
- Data persists across browser sessions
- Clearing browser cache will delete all playlists and progress
- Storage limit: ~5-10MB depending on browser (sufficient for hundreds of playlists)
- Built with Next.js App Router (not Pages Router)
- TypeScript strict mode enabled
- No external dependencies for core functionality
- Web scraping approach may need updates if YouTube changes their HTML structure
- Local storage approach eliminates need for user accounts or databases