# Surprise Me - Quick Start Implementation Guide This guide will help you implement the Surprise Me feature step by step. --- ## 📋 Pre-Implementation Checklist Before starting, ensure you have: - ✅ Next.js project set up - ✅ SQLite database configured - ✅ Existing video browsing functionality - ✅ Understanding of the codebase structure --- ## 🚀 Implementation Steps ### Phase 1: Database Setup (15 minutes) #### Step 1.1: Add Media Access Table Edit `/src/db/index.ts` and add the new table in the `initializeDatabase()` function: ```typescript // Add after the library_cluster_mapping table creation db.exec(` CREATE TABLE IF NOT EXISTS media_access ( id INTEGER PRIMARY KEY AUTOINCREMENT, media_id INTEGER NOT NULL, access_type TEXT NOT NULL CHECK (access_type IN ('view', 'play', 'bookmark', 'rate')), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (media_id) REFERENCES media(id) ON DELETE CASCADE ); `); ``` #### Step 1.2: Add Indexes ```typescript // Add with other indexes db.exec(`CREATE INDEX IF NOT EXISTS idx_media_access_media_id ON media_access(media_id);`); db.exec(`CREATE INDEX IF NOT EXISTS idx_media_access_created_at ON media_access(created_at);`); db.exec(`CREATE INDEX IF NOT EXISTS idx_media_access_type_created_at ON media_access(access_type, created_at);`); ``` #### Step 1.3: Add Helper Functions At the end of `/src/db/index.ts`: ```typescript export function trackMediaAccess(mediaId: number, accessType: 'view' | 'play' | 'bookmark' | 'rate'): void { const db = getDatabase(); db.prepare(` INSERT INTO media_access (media_id, access_type) VALUES (?, ?) `).run(mediaId, accessType); } ``` **Test**: Run your app and check that the database initializes without errors. --- ### Phase 2: Recommendation Service (30 minutes) #### Step 2.1: Create Recommendation Service File Create `/src/lib/recommendation-service.ts`: ```typescript import { getDatabase } from '@/db'; export type RecommendationAlgorithm = | 'smart_mix' | 'weighted_random' | 'forgotten_gems' | 'unwatched_first' | 'pure_random'; export interface RecommendedVideo { id: number; title: string; path: string; size: number; thumbnail: string; type: string; bookmark_count: number; star_count: number; avg_rating: number; created_at: string; recommendation_reason: string; recommendation_score: number; } export class RecommendationService { private db: any; constructor() { this.db = getDatabase(); } async getRecommendations( algorithm: RecommendationAlgorithm = 'smart_mix', limit: number = 20 ): Promise { switch (algorithm) { case 'unwatched_first': return this.getUnwatchedFirst(limit); case 'pure_random': return this.getPureRandom(limit); case 'weighted_random': return this.getWeightedRandom(limit); default: return this.getUnwatchedFirst(limit); // Start simple } } private getUnwatchedFirst(limit: number): RecommendedVideo[] { const query = ` SELECT m.*, 'Never watched before' as recommendation_reason, 1.0 as recommendation_score FROM media m LEFT JOIN media_access ma ON m.id = ma.media_id WHERE m.type = 'video' AND ma.id IS NULL ORDER BY m.created_at DESC LIMIT ? `; return this.db.prepare(query).all(limit); } private getPureRandom(limit: number): RecommendedVideo[] { const query = ` SELECT m.*, 'Random selection' as recommendation_reason, 1.0 as recommendation_score FROM media m WHERE m.type = 'video' ORDER BY RANDOM() LIMIT ? `; return this.db.prepare(query).all(limit); } private getWeightedRandom(limit: number): RecommendedVideo[] { const query = ` SELECT m.*, 'Smart random selection' as recommendation_reason, 1.0 as recommendation_score FROM media m LEFT JOIN media_access ma ON m.id = ma.media_id AND ma.access_type IN ('view', 'play') WHERE m.type = 'video' AND (ma.id IS NULL OR ma.created_at < datetime('now', '-30 days')) ORDER BY RANDOM() LIMIT ? `; return this.db.prepare(query).all(limit); } } ``` **Test**: Import and instantiate the service in a test file to verify it compiles. --- ### Phase 3: API Endpoints (20 minutes) #### Step 3.1: Create Recommendations API Create `/src/app/api/videos/recommendations/route.ts`: ```typescript import { NextRequest, NextResponse } from 'next/server'; import { RecommendationService, RecommendationAlgorithm } from '@/lib/recommendation-service'; export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url); const algorithm = (searchParams.get('algorithm') || 'unwatched_first') as RecommendationAlgorithm; const limit = Math.min(parseInt(searchParams.get('limit') || '20'), 100); const service = new RecommendationService(); const recommendations = await service.getRecommendations(algorithm, limit); return NextResponse.json({ recommendations, algorithm, total_videos: recommendations.length, total_recommended: recommendations.length }); } catch (error: any) { console.error('Recommendation error:', error); return NextResponse.json( { error: error.message || 'Failed to get recommendations' }, { status: 500 } ); } } ``` **Test**: Visit `http://localhost:3000/api/videos/recommendations` in your browser. #### Step 3.2: Create Media Access Tracking API Create `/src/app/api/media-access/[id]/route.ts`: ```typescript import { NextRequest, NextResponse } from 'next/server'; import { trackMediaAccess } from '@/db'; export async function POST( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { try { const { id } = await params; const mediaId = parseInt(id); if (isNaN(mediaId)) { return NextResponse.json( { error: 'Invalid media ID' }, { status: 400 } ); } const body = await request.json(); const { accessType } = body; if (!['view', 'play', 'bookmark', 'rate'].includes(accessType)) { return NextResponse.json( { error: 'Invalid access type' }, { status: 400 } ); } trackMediaAccess(mediaId, accessType); return NextResponse.json({ success: true, mediaId, accessType }); } catch (error: any) { console.error('Access tracking error:', error); return NextResponse.json( { error: error.message || 'Failed to track access' }, { status: 500 } ); } } ``` **Test**: Use Postman or curl to POST to the endpoint. --- ### Phase 4: Sidebar Integration (10 minutes) #### Step 4.1: Add Surprise Me to Sidebar Edit `/src/components/sidebar.tsx`: 1. Import the icon: ```typescript import { Sparkles } from 'lucide-react'; ``` 2. Add the menu item (after Bookmarks, before Clusters): ```typescript {!isCollapsed && Surprise Me} ``` **Test**: Check that the sidebar shows the new item and it's clickable. --- ### Phase 5: Surprise Me Page (45 minutes) #### Step 5.1: Create the Page Create `/src/app/surprise-me/page.tsx`: ```typescript 'use client'; import { useState, useEffect } from 'react'; import { RefreshCw, Sparkles } from 'lucide-react'; import { Button } from '@/components/ui/button'; import UnifiedVideoPlayer from '@/components/unified-video-player'; type Algorithm = 'unwatched_first' | 'pure_random' | 'weighted_random'; const ALGORITHMS = [ { value: 'unwatched_first', label: 'Unwatched First', description: 'Videos you\'ve never seen' }, { value: 'weighted_random', label: 'Weighted Random', description: 'Smart randomization' }, { value: 'pure_random', label: 'Pure Random', description: 'Complete surprise' } ] as const; export default function SurpriseMePage() { const [algorithm, setAlgorithm] = useState('unwatched_first'); const [recommendations, setRecommendations] = useState([]); const [isLoading, setIsLoading] = useState(true); const [selectedVideo, setSelectedVideo] = useState(null); const [isPlayerOpen, setIsPlayerOpen] = useState(false); const fetchRecommendations = async () => { setIsLoading(true); try { const response = await fetch( `/api/videos/recommendations?algorithm=${algorithm}&limit=20` ); const data = await response.json(); setRecommendations(data.recommendations || []); } catch (error) { console.error('Failed to fetch recommendations:', error); } finally { setIsLoading(false); } }; useEffect(() => { fetchRecommendations(); }, [algorithm]); const handleVideoClick = async (video: any) => { try { await fetch(`/api/media-access/${video.id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ accessType: 'view' }) }); } catch (error) { console.error('Failed to track access:', error); } setSelectedVideo(video); setIsPlayerOpen(true); }; return (
{/* Header */}

Surprise Me

Discover videos from your collection

{/* Controls */}
{ALGORITHMS.find(a => a.value === algorithm)?.description}
{/* Recommendations Grid */}
{isLoading ? (

Loading recommendations...

) : recommendations.length === 0 ? (

No recommendations available

Try a different algorithm

) : (
{recommendations.map((video) => (
handleVideoClick(video)} className="group cursor-pointer bg-zinc-900 rounded-lg overflow-hidden border border-zinc-800 hover:border-purple-600 transition-all" >
{video.thumbnail ? ( {video.title} ) : (
)} {video.recommendation_reason && (
{video.recommendation_reason}
)}

{video.title || video.path.split('/').pop()}

{video.avg_rating > 0 && ( ⭐ {video.avg_rating.toFixed(1)} )} {video.bookmark_count > 0 && ( 🔖 )}
))}
)}
{/* Video Player */} {selectedVideo && ( { setIsPlayerOpen(false); setSelectedVideo(null); }} playerType="modal" useArtPlayer={true} /> )}
); } ``` **Test**: Navigate to `/surprise-me` and verify the page loads. --- ## ✅ Testing Checklist ### Manual Testing - [ ] Database table created successfully - [ ] Sidebar shows "Surprise Me" item - [ ] Clicking sidebar item navigates to `/surprise-me` - [ ] Page loads without errors - [ ] Algorithm dropdown works - [ ] Refresh button works - [ ] Video cards display correctly - [ ] Clicking video opens player - [ ] Player closes correctly - [ ] Different algorithms show different results - [ ] Access tracking works (check database) ### API Testing ```bash # Test recommendations endpoint curl http://localhost:3000/api/videos/recommendations # Test with algorithm curl http://localhost:3000/api/videos/recommendations?algorithm=pure_random&limit=10 # Test access tracking curl -X POST http://localhost:3000/api/media-access/1 \ -H "Content-Type: application/json" \ -d '{"accessType": "view"}' ``` --- ## 🐛 Common Issues & Solutions ### Issue: No recommendations showing **Solution**: Check if you have videos in the database. Run: ```sql SELECT COUNT(*) FROM media WHERE type = 'video'; ``` ### Issue: Database error on startup **Solution**: Delete `data/media.db` and restart to recreate schema. ### Issue: API returns 500 error **Solution**: Check server logs for detailed error messages. ### Issue: Sidebar item not showing **Solution**: Verify you imported `Sparkles` icon and added the Link component correctly. --- ## 🎯 Next Steps After basic implementation: 1. Add more algorithms (Forgotten Gems, Similar to Favorites) 2. Improve UI with animations and transitions 3. Add statistics (total unwatched, most recommended, etc.) 4. Implement caching for better performance 5. Add user preferences for default algorithm --- ## 📚 Additional Resources - [Complete Design Document](SURPRISE_ME_RECOMMENDATION_DESIGN.md) - [Architecture Diagrams](SURPRISE_ME_ARCHITECTURE_DIAGRAM.md) - [Implementation Examples](SURPRISE_ME_IMPLEMENTATION_EXAMPLES.md) - [Quick Summary](SURPRISE_ME_SUMMARY.md) --- **Estimated Total Time**: 2-3 hours for basic implementation **Difficulty**: Intermediate **Prerequisites**: - Next.js knowledge - SQLite/SQL basics - React hooks understanding - TypeScript familiarity